From 014e130d4888a2b7f8661226de243dcee46e6b7c Mon Sep 17 00:00:00 2001 From: Saqib Dhuka Date: Wed, 6 Oct 2021 17:45:05 +0000 Subject: [PATCH 01/35] StepFunctionsRestApi implemented along with unit and integration testing. 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. Created a builder class to make the long request template string more readable. closes aws#15081. --- packages/@aws-cdk/aws-apigateway/README.md | 26 + packages/@aws-cdk/aws-apigateway/lib/index.ts | 1 + .../integrations/execution-input-builder.ts | 846 ++++++++++++++++++ .../aws-apigateway/lib/integrations/index.ts | 2 + .../lib/integrations/stepfunctions.ts | 195 ++++ .../aws-apigateway/lib/stepfunctions-api.ts | 119 +++ packages/@aws-cdk/aws-apigateway/package.json | 3 + ...unctions-api.deploymentStack.expected.json | 261 ++++++ ...integ.stepFunctions-api.deploymentStack.ts | 33 + .../test/integrations/stepFunctions.test.ts | 247 +++++ .../test/stepFunctions-api.test.ts | 329 +++++++ 11 files changed, 2062 insertions(+) create mode 100644 packages/@aws-cdk/aws-apigateway/lib/integrations/execution-input-builder.ts create mode 100644 packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts create mode 100644 packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.stepFunctions-api.deploymentStack.expected.json create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.stepFunctions-api.deploymentStack.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/integrations/stepFunctions.test.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/stepFunctions-api.test.ts diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 4789a8cc62410..efb40b782ed4d 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -22,6 +22,7 @@ running on AWS Lambda, or any web application. - [Defining APIs](#defining-apis) - [Breaking up Methods and Resources across Stacks](#breaking-up-methods-and-resources-across-stacks) - [AWS Lambda-backed APIs](#aws-lambda-backed-apis) +- [AWS StepFunctions backed APIs](#aws-stepfunctions-backed-APIs) - [Integration Targets](#integration-targets) - [Usage Plan & API Keys](#usage-plan--api-keys) - [Working with models](#working-with-models) @@ -106,6 +107,31 @@ item.addMethod('GET'); // GET /items/{item} item.addMethod('DELETE', new apigateway.HttpIntegration('http://amazon.com')); ``` +## AWS StepFunctions backed APIs + +You can use Amazon API Gateway with AWS Step Functions as the backend integration, specifically Synchronous Express Workflows. + +The `StepFunctionsRestApi` construct makes this easy and also sets up input, output and error mapping. The `StepFunctionsRestApi` construct sets up the API Gateway REST API with an `ANY` HTTP method and sets up the api role with the required permission to invoke `StartSyncExecution` action on the AWS StepFunctions state machine. This will enable you to invoke any one of the API Gateway HTTP methods and get a response from the backend AWS StepFunctions state machine. + +The following code defines a REST API that routes all requests to the specified AWS StepFunctions state machine: + +```ts +const stateMachine = new stepFunctions.StateMachine(this, 'StateMachine', ...); +new apigateway.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { + stateMachine: stateMachine, +}); +``` + +You can add requestContext (similar to input requestContext from lambda input) to the input. The 'requestContext' parameter includes account ID, user identity, etc. that can be used by customers that want to know the identity of authorized users on the state machine side. The following code defines a REST API like above but also adds 'requestContext' to the input of the State Machine: + +```ts +const stateMachine = new stepFunctions.StateMachine(this, 'StateMachine', ...); +new apigateway.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { + stateMachine: stateMachine, + includeRequestContext: true, +}); +``` + ### Breaking up Methods and Resources across Stacks It is fairly common for REST APIs with a large number of Resources and Methods to hit the [CloudFormation diff --git a/packages/@aws-cdk/aws-apigateway/lib/index.ts b/packages/@aws-cdk/aws-apigateway/lib/index.ts index 4c288b27f4160..58b8150e2ebf9 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/index.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/index.ts @@ -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'; diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/execution-input-builder.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/execution-input-builder.ts new file mode 100644 index 0000000000000..aacda0821ae40 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/execution-input-builder.ts @@ -0,0 +1,846 @@ +/** + * Builder to build execution input object + */ +export class ExecutionInputBuilder { + /** + * Input body string + */ + private _bodyStr: string; + + /** + * Context start string + */ + private _contextStr: string | undefined; + + /** + * Account ID string + */ + private _accountIdStr: string | undefined; + + /** + * Api ID string + */ + private _apiIdStr: string | undefined; + + /** + * Api Key string + */ + private _apiKeyStr: string | undefined; + + /** + * Authorizer Principal ID string + */ + private _authorizerPrincipalIdStr: string | undefined; + + /** + * Caller string + */ + private _callerStr: string | undefined; + + /** + * Cognito Authentication Provider string + */ + private _cognitoAuthenticationProviderStr: string | undefined; + + /** + * Cognito Authentication Type string + */ + private _cognitoAuthenticationTypeStr: string | undefined; + + /** + * Cognito Identity ID string + */ + private _cognitoIdentityIdStr: string | undefined; + + /** + * Cognito Identity Pool ID string + */ + private _cognitoIdentityPoolIdStr: string | undefined; + + /** + * Http Method string + */ + private _httpMethodStr: string | undefined; + + /** + * Stage string + */ + private _stageStr: string | undefined; + + /** + * Source IP string + */ + private _sourceIpStr: string | undefined; + + /** + * User string + */ + private _userStr: string | undefined; + + /** + * User Agent string + */ + private _userAgentStr: string | undefined; + + /** + * User Arn string + */ + private _userArnStr: string | undefined; + + /** + * Request ID string + */ + private _requestIdStr: string | undefined; + + /** + * Resource ID string + */ + private _resourceIdStr: string | undefined; + + /** + * Resource Path string + */ + private _resourcePathStr: string | undefined; + + /** + * End string + */ + private _endStr: string | undefined; + + constructor(input: string) { + this._bodyStr = '"body": ' + input + ','; + } + + /** + * set contextStr + * @param _context + * @returns ExecutionInputBuilder + */ + public withContext(_context: string | undefined): ExecutionInputBuilder { + if (_context == null) { + this._contextStr = ''; + return this; + } + this._contextStr = _context; + return this; + } + + /** + * set accountIdStr + * @param _accountId + * @returns ExecutionInputBuilder + */ + public withAccountId(_accountId: string | undefined): ExecutionInputBuilder { + if (_accountId == null) { + this._accountIdStr = ''; + return this; + } + this._accountIdStr = '"accountId":' + _accountId + ','; + return this; + } + + /** + * set apiIdStr + * @param _apiId + * @returns ExecutionInputBuilder + */ + public withApiId(_apiId: string | undefined): ExecutionInputBuilder { + if (_apiId == null) { + this._apiIdStr = ''; + return this; + } + this._apiIdStr = '"apiId":' + _apiId + ','; + return this; + } + + /** + * set apiKeyStr + * @param _apiKey + * @returns ExecutionInputBuilder + */ + public withApiKey(_apiKey: string | undefined): ExecutionInputBuilder { + if (_apiKey == null) { + this._apiKeyStr = ''; + return this; + } + this._apiKeyStr = '"apiKey":' + _apiKey + ','; + return this; + } + + /** + * set _authorizerPrincipalIdStr + * @param _authorizerPrincipalId + * @returns ExecutionInputBuilder + */ + public withAuthorizerPrincipalId(_authorizerPrincipalId: string | undefined): ExecutionInputBuilder { + if (_authorizerPrincipalId == null) { + this._authorizerPrincipalIdStr = ''; + return this; + } + this._authorizerPrincipalIdStr = '"authorizerPrincipalId":' + _authorizerPrincipalId + ','; + return this; + } + + /** + * set _callerStr + * @param _caller + * @returns ExecutionInputBuilder + */ + public withCaller(_caller: string | undefined): ExecutionInputBuilder { + if (_caller == null) { + this._callerStr = ''; + return this; + } + this._callerStr = '"caller":' + _caller + ','; + return this; + } + + /** + * set _cognitoAuthenticationProviderStr + * @param _cognitoAuthenticationProvider + * @returns ExecutionInputBuilder + */ + public withCognitoAuthenticationProvider(_cognitoAuthenticationProvider: string | undefined): ExecutionInputBuilder { + if (_cognitoAuthenticationProvider == null) { + this._cognitoAuthenticationProviderStr = ''; + return this; + } + this._cognitoAuthenticationProviderStr = '"cognitoAuthenticationProvider":' + _cognitoAuthenticationProvider + ','; + return this; + } + + /** + * set _cognitoAuthenticationTypeStr + * @param _cognitoAuthenticationType + * @returns ExecutionInputBuilder + */ + public withCognitoAuthenticationType(_cognitoAuthenticationType: string | undefined): ExecutionInputBuilder { + if (_cognitoAuthenticationType == null) { + this._cognitoAuthenticationTypeStr = ''; + return this; + } + this._cognitoAuthenticationTypeStr = '"cognitoAuthenticationType":' + _cognitoAuthenticationType + ','; + return this; + } + + /** + * set _cognitoIdentityIdStr + * @param _cognitoIdentityId + * @returns ExecutionInputBuilder + */ + public withCognitoIdentityId(_cognitoIdentityId: string | undefined): ExecutionInputBuilder { + if (_cognitoIdentityId == null) { + this._cognitoIdentityIdStr = ''; + return this; + } + this._cognitoIdentityIdStr = '"cognitoIdentityId":' + _cognitoIdentityId + ','; + return this; + } + + /** + * set _cognitoIdentityPoolIdStr + * @param _cognitoIdentityPoolId + * @returns ExecutionInputBuilder + */ + public withCognitoIdentityPoolId(_cognitoIdentityPoolId: string | undefined): ExecutionInputBuilder { + if (_cognitoIdentityPoolId == null) { + this._cognitoIdentityPoolIdStr = ''; + return this; + } + this._cognitoIdentityPoolIdStr = '"cognitoIdentityPoolId":' + _cognitoIdentityPoolId + ','; + return this; + } + + /** + * set _httpMethodStr + * @param _httpMethod + * @returns ExecutionInputBuilder + */ + public withHttpMethod(_httpMethod: string | undefined): ExecutionInputBuilder { + if (_httpMethod == null) { + this._httpMethodStr = ''; + return this; + } + this._httpMethodStr = '"httpMethod":' + _httpMethod + ','; + return this; + } + + /** + * set _stageStr + * @param _stage + * @returns ExecutionInputBuilder + */ + public withStage(_stage: string | undefined): ExecutionInputBuilder { + if (_stage == null) { + this._stageStr = ''; + return this; + } + this._stageStr = '"stage":' + _stage + ','; + return this; + } + + /** + * set _sourceIpStr + * @param _sourceIp + * @returns ExecutionInputBuilder + */ + public withSourceIp(_sourceIp: string | undefined): ExecutionInputBuilder { + if (_sourceIp == null) { + this._sourceIpStr = ''; + return this; + } + this._sourceIpStr = '"sourceIp":' + _sourceIp + ','; + return this; + } + + /** + * set _userStr + * @param _user + * @returns ExecutionInputBuilder + */ + public withUser(_user: string | undefined): ExecutionInputBuilder { + if (_user == null) { + this._userStr = ''; + return this; + } + this._userStr = '"user":' + _user + ','; + return this; + } + + /** + * set _userAgentStr + * @param _userAgent + * @returns ExecutionInputBuilder + */ + + public withUserAgent(_userAgent: string | undefined): ExecutionInputBuilder { + if (_userAgent == null) { + this._userAgentStr = ''; + return this; + } + this._userAgentStr = '"userAgent":' + _userAgent + ','; + return this; + } + + /** + * set _userArnStr + * @param _userArn + * @returns ExecutionInputBuilder + */ + public withUserArn(_userArn: string | undefined): ExecutionInputBuilder { + if (_userArn == null) { + this._userArnStr = ''; + return this; + } + this._userArnStr = '"userArn":' + _userArn + ','; + return this; + } + + /** + * set _requestIdStr + * @param _requestId + * @returns ExecutionInputBuilder + */ + public withRequestId(_requestId: string | undefined): ExecutionInputBuilder { + if (_requestId == null) { + this._requestIdStr = ''; + return this; + } + this._requestIdStr = '"requestId":' + _requestId + ','; + return this; + } + + /** + * set _resourceIdStr + * @param _resourceId + * @returns ExecutionInputBuilder + */ + public withResourceId(_resourceId: string | undefined): ExecutionInputBuilder { + if (_resourceId == null) { + this._resourceIdStr = ''; + return this; + } + this._resourceIdStr = '"resourceId":' + _resourceId + ','; + return this; + } + + /** + * set _resourcePathStr + * @param _resourcePath + * @returns ExecutionInputBuilder + */ + public withResourcePath(_resourcePath: string | undefined): ExecutionInputBuilder { + if (_resourcePath == null) { + this._resourcePathStr = ''; + return this; + } + this._resourcePathStr = '"resourcePath":' + _resourcePath; + return this; + } + + /** + * set _endStr + * @param _end + * @returns ExecutionInputBuilder + */ + public withEnd(_end: string | undefined): ExecutionInputBuilder { + if ( _end == null) { + this._endStr = ''; + return this; + } + this._endStr = _end; + return this; + } + + /** + * returns _bodystr + */ + public get retrieveBodyStr() { + return this._bodyStr; + } + + /** + * returns _contextStr + */ + public get retrieveContextStr() { + return this._contextStr; + } + + /** + * returns _accountIdStr + */ + public get retrieveAccountIdStr() { + return this._accountIdStr; + } + + /** + * returns _apiIdStr + */ + public get retrieveApiIdStr() { + return this._apiIdStr; + } + + /** + * returns _apiKeyStr + */ + public get retrieveApiKeyStr() { + return this._apiKeyStr; + } + + /** + * returns _authorizerPrincipalIdStr + */ + public get retrieveAuthorizerPrincipalIdStr() { + return this._authorizerPrincipalIdStr; + } + + /** + * returns _callerStr + */ + public get retrieveCallerStr() { + return this._callerStr; + } + + /** + * returns _cognitoAuthenticationProviderStr + */ + public get retrieveCognitoAuthenticationProviderStr() { + return this._cognitoAuthenticationProviderStr; + } + + /** + * returns _cognitoAuthenticationTypeStr + */ + public get retrieveCognitoAuthenticationTypeStr() { + return this._cognitoAuthenticationTypeStr; + } + + /** + * returns _cognitoIdentityIdStr + */ + public get retrieveCognitoIdentityIdStr() { + return this._cognitoIdentityIdStr; + } + + /** + * returns _cognitoIdentityPoolIdStr + */ + public get retrieveCognitoIdentityPoolIdStr() { + return this._cognitoIdentityPoolIdStr; + } + + /** + * returns _httpMethodStr + */ + public get retrieveHttpMethodStr() { + return this._httpMethodStr; + } + + /** + * returns _stageStr + */ + public get retrieveStageStr() { + return this._stageStr; + } + + /** + * returns _sourceIpStr + */ + public get retrieveSourceIpStr() { + return this._sourceIpStr; + } + + /** + * returns _userStr + */ + public get retrieveUserStr() { + return this._userStr; + } + + /** + * returns _userAgentStr + */ + public get retrieveUserAgentStr() { + return this._userAgentStr; + } + + /** + * returns _userArnStr + */ + public get retrieveUserArnStr() { + return this._userArnStr ; + } + + /** + * returns _requestIdStr + */ + public get retrieveRequestIdStr() { + return this._requestIdStr; + } + + /** + * returns _resourceIdStr + */ + public get retrieveResourceIdStr() { + return this._resourceIdStr; + } + + /** + * returns _resourcePathStr + */ + public get retrieveResourcePathStr() { + return this._resourcePathStr; + } + + /** + * returns _endstr + */ + public get retrieveEndStr() { + return this._endStr; + } + + /** + * Returns object + * @returns ExecutionInput + */ + public create(): ExecutionInput { + return new ExecutionInput(this); + } + +} + +/** + * execution input object + */ +export class ExecutionInput { + /** + * Input body string + */ + private _bodyStr: string; + + /** + * Context start string + */ + private _contextStr: string | undefined; + + /** + * Account ID string + */ + private _accountIdStr: string | undefined; + + /** + * Api ID string + */ + private _apiIdStr: string | undefined; + + /** + * Api Key string + */ + private _apiKeyStr: string | undefined; + + /** + * Authorizer Principal ID string + */ + private _authorizerPrincipalIdStr: string | undefined; + + /** + * Caller string + */ + private _callerStr: string | undefined; + + /** + * Cognito Authentication Provider string + */ + private _cognitoAuthenticationProviderStr: string | undefined; + + /** + * Cognito Authentication Type string + */ + private _cognitoAuthenticationTypeStr: string | undefined; + + /** + * Cognito Identity ID string + */ + private _cognitoIdentityIdStr: string | undefined; + + /** + * Cognito Identity Pool ID string + */ + private _cognitoIdentityPoolIdStr: string | undefined; + + /** + * Http Method string + */ + private _httpMethodStr: string | undefined; + + /** + * Stage string + */ + private _stageStr: string | undefined; + + /** + * Source IP string + */ + private _sourceIpStr: string | undefined; + + /** + * User string + */ + private _userStr: string | undefined; + + /** + * User Agent string + */ + private _userAgentStr: string | undefined; + + /** + * User Arn string + */ + private _userArnStr: string | undefined; + + /** + * Request ID string + */ + private _requestIdStr: string | undefined; + + /** + * Resource ID string + */ + private _resourceIdStr: string | undefined; + + /** + * Resource Path string + */ + private _resourcePathStr: string | undefined; + + /** + * End string + */ + private _endStr: string | undefined; + + constructor(builder: ExecutionInputBuilder) { + this._bodyStr = builder.retrieveBodyStr; + this._contextStr = builder.retrieveContextStr; + this._accountIdStr = builder.retrieveAccountIdStr; + this._apiIdStr = builder.retrieveApiIdStr; + this._apiKeyStr = builder.retrieveApiKeyStr; + this._authorizerPrincipalIdStr = builder.retrieveAuthorizerPrincipalIdStr; + this._callerStr = builder.retrieveCallerStr; + this._cognitoAuthenticationProviderStr = builder.retrieveCognitoAuthenticationProviderStr; + this._cognitoAuthenticationTypeStr = builder.retrieveCognitoAuthenticationTypeStr; + this._cognitoIdentityIdStr = builder.retrieveCognitoIdentityIdStr; + this._cognitoIdentityPoolIdStr = builder.retrieveCognitoIdentityPoolIdStr; + this._httpMethodStr = builder.retrieveHttpMethodStr; + this._stageStr = builder.retrieveStageStr; + this._sourceIpStr = builder.retrieveSourceIpStr; + this._userStr = builder.retrieveUserStr; + this._userAgentStr = builder.retrieveUserAgentStr; + this._userArnStr = builder.retrieveUserArnStr; + this._requestIdStr = builder.retrieveRequestIdStr; + this._resourceIdStr = builder.retrieveResourceIdStr; + this._resourcePathStr = builder.retrieveResourcePathStr; + this._endStr = builder.retrieveEndStr; + } + + /** + * returns _bodystr + */ + public get retrieveBodyStr() { + return this._bodyStr; + } + + /** + * returns _contextStr + */ + public get retrieveContextStr() { + return this._contextStr; + } + + /** + * returns _accountIdStr + */ + public get retrieveAccountIdStr() { + return this._accountIdStr; + } + + /** + * returns _apiIdStr + */ + public get retrieveApiIdStr() { + return this._apiIdStr; + } + + /** + * returns _apiKeyStr + */ + public get retrieveApiKeyStr() { + return this._apiKeyStr; + } + + /** + * returns _authorizerPrincipalIdStr + */ + public get retrieveAuthorizerPrincipalIdStr() { + return this._authorizerPrincipalIdStr; + } + + /** + * returns _callerStr + */ + public get retrieveCallerStr() { + return this._callerStr; + } + + /** + * returns _cognitoAuthenticationProviderStr + */ + public get retrieveCognitoAuthenticationProviderStr() { + return this._cognitoAuthenticationProviderStr; + } + + /** + * returns _cognitoAuthenticationTypeStr + */ + public get retrieveCognitoAuthenticationTypeStr() { + return this._cognitoAuthenticationTypeStr; + } + + /** + * returns _cognitoIdentityIdStr + */ + public get retrieveCognitoIdentityIdStr() { + return this._cognitoIdentityIdStr; + } + + /** + * returns _cognitoIdentityPoolIdStr + */ + public get retrieveCognitoIdentityPoolIdStr() { + return this._cognitoIdentityPoolIdStr; + } + + /** + * returns _httpMethodStr + */ + public get retrieveHttpMethodStr() { + return this._httpMethodStr; + } + + /** + * returns _stageStr + */ + public get retrieveStageStr() { + return this._stageStr; + } + + /** + * returns _sourceIpStr + */ + public get retrieveSourceIpStr() { + return this._sourceIpStr; + } + + /** + * returns _userStr + */ + public get retrieveUserStr() { + return this._userStr; + } + + /** + * returns _userAgentStr + */ + public get retrieveUserAgentStr() { + return this._userAgentStr; + } + + /** + * returns _userArnStr + */ + public get retrieveUserArnStr() { + return this._userArnStr ; + } + + /** + * returns _requestIdStr + */ + public get retrieveRequestIdStr() { + return this._requestIdStr; + } + + /** + * returns _resourceIdStr + */ + public get retrieveResourceIdStr() { + return this._resourceIdStr; + } + + /** + * returns _resourcePathStr + */ + public get retrieveResourcePathStr() { + return this._resourcePathStr; + } + + /** + * returns _endstr + */ + public get retrieveEndStr() { + return this._endStr; + } + + /** + * Returns all properties as a single single + */ + public retrieveAllAsString(): string { + const executionInputStr :string = this.retrieveBodyStr + this.retrieveContextStr + this.retrieveAccountIdStr + + this.retrieveApiIdStr + this.retrieveApiKeyStr + this.retrieveAuthorizerPrincipalIdStr + + this.retrieveCallerStr + this.retrieveCognitoAuthenticationProviderStr + + this.retrieveCognitoAuthenticationTypeStr + this.retrieveCognitoIdentityIdStr + + this.retrieveCognitoIdentityPoolIdStr + this.retrieveHttpMethodStr + + this.retrieveStageStr + this.retrieveSourceIpStr + this.retrieveUserStr + this.retrieveUserAgentStr + + this.retrieveUserArnStr + this.retrieveRequestIdStr + this.retrieveResourceIdStr + this.retrieveResourcePathStr + this.retrieveEndStr; + + return executionInputStr; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/index.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/index.ts index 1369c366d655f..a1590114a131f 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/index.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/index.ts @@ -2,3 +2,5 @@ export * from './aws'; export * from './lambda'; export * from './http'; export * from './mock'; +export * from './stepfunctions'; +export * from './execution-input-builder'; diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts new file mode 100644 index 0000000000000..4666978d32c4f --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts @@ -0,0 +1,195 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Token } from '@aws-cdk/core'; +import { ExecutionInput, ExecutionInputBuilder } from '.'; +import { IntegrationConfig, IntegrationOptions, PassthroughBehavior } from '../integration'; +import { Method } from '../method'; +import { AwsIntegration } from './aws'; + +/** + * Options when configuring Step Functions integration with Rest API + */ +export interface StepFunctionsIntegrationOptions extends IntegrationOptions { + /** + * 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. This requestContext is same as the lambda input (https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format) requestContext parameter. + * @default false + */ + readonly includeRequestContext?: boolean; + +} +/** + * Integrates a Synchronous Express State Machine from AWS Step Functions to an API Gateway method. + * + * @example + * + * const stateMachine = new sfn.StateMachine(this, 'MyStateMachine', ...); + * api.addMethod('GET', new StepFunctionsIntegration(stateMachine)); + */ +export class StepFunctionsIntegration extends AwsIntegration { + private readonly stateMachine: sfn.IStateMachine; + + constructor(stateMachine: sfn.IStateMachine, options: StepFunctionsIntegrationOptions = { }) { + + const integResponse = integrationResponse(); + const requestTemplate = requestTemplates(stateMachine, options.includeRequestContext); + super({ + service: 'states', + action: 'StartSyncExecution', + options: { + credentialsRole: options.credentialsRole, + integrationResponses: integResponse, + passthroughBehavior: PassthroughBehavior.NEVER, + requestTemplates: requestTemplate, + }, + }); + + this.stateMachine = stateMachine; + } + + public bind(method: Method): IntegrationConfig { + const bindResult = super.bind(method); + const principal = new iam.ServicePrincipal('apigateway.amazonaws.com'); + + this.stateMachine.grantExecution(principal, 'states:StartSyncExecution'); + + let stateMachineName; + + if (this.stateMachine 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.stateMachine.node.defaultChild as sfn.CfnStateMachine).stateMachineName; + } else { + //imported state machine + stateMachineName = 'StateMachine-' + (String(this.stateMachine.stack.node.addr).substring(0, 8)); + } + + let deploymentToken; + + if (stateMachineName !== undefined && !Token.isUnresolved(stateMachineName)) { + deploymentToken = JSON.stringify({ stateMachineName }); + } + return { + ...bindResult, + deploymentToken, + }; + + } +} + +function integrationResponse() { + const errorResponse = [ + { + /** + * Specifies the regular expression (regex) pattern used to choose + * an integration response based on the response from the back end. + * In this case it will match all '4XX' HTTP Errors + */ + selectionPattern: '4\\d{2}', + statusCode: '400', + responseTemplates: { + 'application/json': `{ + "error": "Bad input!" + }`, + }, + }, + { + /** + * Match all '5XX' HTTP Errors + */ + 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 requestTemplates(stateMachine: sfn.IStateMachine, includeRequestContext: boolean | undefined) { + const templateStr = templateString(stateMachine, includeRequestContext); + + const requestTemplate: { [contentType:string] : string } = + { + 'application/json': templateStr, + }; + + return requestTemplate; +} + +function templateString(stateMachine: sfn.IStateMachine, includeRequestContext: boolean | undefined): string { + let templateStr: string; + const requestContextStr = requestContext(); + + const search = '"'; + const replaceWith = '\\"'; + if (typeof includeRequestContext === 'boolean' && includeRequestContext === true) { + templateStr = ` + #set($allParams = $input.params()) + { + "input": "{${(requestContextStr.split(search).join(replaceWith))}}", + "stateMachineArn": "${stateMachine.stateMachineArn}" + }`; + } else { + templateStr = ` + #set($inputRoot = $input.path('$')) { + "input": "$util.escapeJavaScript($input.json('$'))", + "stateMachineArn": "${stateMachine.stateMachineArn}" + }`; + } + return templateStr; +} + +function requestContext(): string { + const executionInput: ExecutionInput = new ExecutionInputBuilder('$util.escapeJavaScript($input.json(\'$\'))') + .withContext('"requestContext": {') + .withAccountId('"$context.identity.accountId"') + .withApiId('"$context.apiId"') + .withApiKey('"$context.identity.apiKey"') + .withAuthorizerPrincipalId('"$context.authorizer.principalId"') + .withCaller('"$context.identity.caller"') + .withCognitoAuthenticationProvider('"$context.identity.cognitoAuthenticationProvider"') + .withCognitoAuthenticationType('"$context.identity.cognitoAuthenticationType"') + .withCognitoIdentityId('"$context.identity.cognitoIdentityId"') + .withCognitoIdentityPoolId('"$context.identity.cognitoIdentityPoolId"') + .withHttpMethod('"$context.httpMethod"') + .withStage('"$context.stage"') + .withSourceIp('"$context.identity.sourceIp"') + .withUser('"$context.identity.user"') + .withUserAgent('"$context.identity.userAgent"') + .withUserArn('"$context.identity.userArn"') + .withRequestId('"$context.requestId"') + .withResourceId('"$context.resourceId"') + .withResourcePath('"$context.resourcePath"') + .withEnd('}') + .create(); + + return executionInput.retrieveAllAsString(); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts b/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts new file mode 100644 index 0000000000000..e6c3f0e196cfb --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts @@ -0,0 +1,119 @@ +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'; + +/** + * Propeties for StepFunctionsRestApi + * + */ +export interface StepFunctionsRestApiProps extends RestApiProps { +/** + * The default State Machine that handles all requests from this API. + * + * This stateMachine will be used as a the default integration for all methods in + * this API, unless specified otherwise in `addMethod`. + */ + readonly stateMachine: sfn.IStateMachine; + + /** + * Check if requestContext is enabled + * If enabled, requestContext is passed into the input of the State Machine. This requestContext is same as the lambda input (https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format) requestContext parameter. + * @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.stateMachine.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 = role(scope, props); + const methodResp = methodResponse(); + + let corsEnabled; + + if (props.defaultCorsPreflightOptions !== undefined) { + corsEnabled = true; + } else { + corsEnabled = false; + } + + super(scope, id, { + defaultIntegration: new StepFunctionsIntegration(props.stateMachine, { + credentialsRole: apiRole, + corsEnabled: corsEnabled, + includeRequestContext: props.includeRequestContext, + }), + ...props, + }); + + if (!corsEnabled) { + this.root.addMethod('ANY', new StepFunctionsIntegration(props.stateMachine, { + credentialsRole: apiRole, + includeRequestContext: props.includeRequestContext, + }), { + methodResponses: [ + ...methodResp, + ], + }); + } + } +} + +function role(scope: Construct, props: StepFunctionsRestApiProps): iam.Role { + const apiName: string = props.stateMachine + '-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.stateMachine.stateMachineArn], + }), + ], + }), + ); + + return apiRole; +} + +function methodResponse() { + 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; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/package.json b/packages/@aws-cdk/aws-apigateway/package.json index 132082167f0f6..2b0793b3adf95 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -92,6 +92,7 @@ "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/cx-api": "0.0.0", + "@aws-cdk/aws-stepfunctions": "0.0.0", "constructs": "^3.3.69" }, "homepage": "https://github.com/aws/aws-cdk", @@ -108,6 +109,7 @@ "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/cx-api": "0.0.0", + "@aws-cdk/aws-stepfunctions": "0.0.0", "constructs": "^3.3.69" }, "engines": { @@ -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", diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepFunctions-api.deploymentStack.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.stepFunctions-api.deploymentStack.expected.json new file mode 100644 index 0000000000000..cc3ccb6c6484b --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.stepFunctions-api.deploymentStack.expected.json @@ -0,0 +1,261 @@ +{ + "Resources": { + "StateMachineRoleB840431D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, + "DefinitionString": "{\"StartAt\":\"PassTask\",\"States\":{\"PassTask\":{\"Type\":\"Pass\",\"Result\":\"Hello\",\"End\":true}}}", + "StateMachineType": "EXPRESS" + }, + "DependsOn": [ + "StateMachineRoleB840431D" + ] + }, + "StepFunctionsRestApiDeploymentStackStateMachineapiRoleE9B057CB": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "AllowStartSyncExecutionE0A8041C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartSyncExecution", + "Effect": "Allow", + "Resource": { + "Ref": "StateMachine2E01A3A5" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AllowStartSyncExecutionE0A8041C", + "Roles": [ + { + "Ref": "StepFunctionsRestApiDeploymentStackStateMachineapiRoleE9B057CB" + } + ] + } + }, + "StepFunctionsRestApiC6E3E883": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "StepFunctionsRestApi" + } + }, + "StepFunctionsRestApiCloudWatchRoleB06ACDB9": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + } + }, + "StepFunctionsRestApiAccountBD0CCC0E": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "StepFunctionsRestApiCloudWatchRoleB06ACDB9", + "Arn" + ] + } + }, + "DependsOn": [ + "StepFunctionsRestApiC6E3E883" + ] + }, + "StepFunctionsRestApiANY7699CA92": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Fn::GetAtt": [ + "StepFunctionsRestApiC6E3E883", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "StepFunctionsRestApiC6E3E883" + }, + "AuthorizationType": "NONE", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "StepFunctionsRestApiDeploymentStackStateMachineapiRoleE9B057CB", + "Arn" + ] + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": [ + { + "ResponseTemplates": { + "application/json": "#set($inputRoot = $input.path('$'))\n #if($input.path('$.status').toString().equals(\"FAILED\"))\n #set($context.responseOverride.status = 500)\n { \n \"error\": \"$input.path('$.error')\",\n \"cause\": \"$input.path('$.cause')\"\n }\n #else\n $input.path('$.output')\n #end" + }, + "StatusCode": "200" + }, + { + "ResponseTemplates": { + "application/json": "{\n \"error\": \"Bad input!\"\n }" + }, + "SelectionPattern": "4\\d{2}", + "StatusCode": "400" + }, + { + "ResponseTemplates": { + "application/json": "\"error\": $input.path('$.error')" + }, + "SelectionPattern": "5\\d{2}", + "StatusCode": "500" + } + ], + "PassthroughBehavior": "NEVER", + "RequestTemplates": { + "application/json": { + "Fn::Join": [ + "", + [ + "\n #set($inputRoot = $input.path('$')) {\n \"input\": \"$util.escapeJavaScript($input.json('$'))\",\n \"stateMachineArn\": \"", + { + "Ref": "StateMachine2E01A3A5" + }, + "\"\n }" + ] + ] + } + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":states:action/StartSyncExecution" + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseModels": { + "application/json": "Empty" + }, + "StatusCode": "200" + }, + { + "ResponseModels": { + "application/json": "Error" + }, + "StatusCode": "400" + }, + { + "ResponseModels": { + "application/json": "Error" + }, + "StatusCode": "500" + } + ] + } + }, + "deployment33381975b5dafda9a97138f301ea25da405640e8": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "StepFunctionsRestApiC6E3E883" + } + }, + "DependsOn": [ + "StepFunctionsRestApiANY7699CA92" + ] + }, + "stage0661E4AC": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "StepFunctionsRestApiC6E3E883" + }, + "DeploymentId": { + "Ref": "deployment33381975b5dafda9a97138f301ea25da405640e8" + }, + "StageName": "prod" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepFunctions-api.deploymentStack.ts b/packages/@aws-cdk/aws-apigateway/test/integ.stepFunctions-api.deploymentStack.ts new file mode 100644 index 0000000000000..14719e9ef901a --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.stepFunctions-api.deploymentStack.ts @@ -0,0 +1,33 @@ +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as apigw from '../lib'; + +class StepFunctionsRestApiDeploymentStack extends cdk.Stack { + constructor(scope: Construct) { + super(scope, 'StepFunctionsRestApiDeploymentStack'); + + const passTask = new sfn.Pass(this, 'PassTask', { + result: { value: 'Hello' }, + }); + + const stateMachine = new sfn.StateMachine(this, 'StateMachine', { + definition: passTask, + stateMachineType: sfn.StateMachineType.EXPRESS, + }); + + const api = new apigw.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { + deploy: false, + stateMachine: stateMachine, + }); + + api.deploymentStage = new apigw.Stage(this, 'stage', { + deployment: new apigw.Deployment(this, 'deployment', { + api, + }), + }); + } +} + +const app = new cdk.App(); +new StepFunctionsRestApiDeploymentStack(app); diff --git a/packages/@aws-cdk/aws-apigateway/test/integrations/stepFunctions.test.ts b/packages/@aws-cdk/aws-apigateway/test/integrations/stepFunctions.test.ts new file mode 100644 index 0000000000000..98c01f97b60b7 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integrations/stepFunctions.test.ts @@ -0,0 +1,247 @@ +import '@aws-cdk/assert-internal/jest'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { StateMachine } from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import * as apigw from '../../lib'; + +function givenSetup() { + const stack = new cdk.Stack(); + const api = new apigw.RestApi(stack, 'my-rest-api'); + const passTask = new sfn.Pass(stack, 'passTask', { + inputPath: '$.somekey', + }); + + const stateMachine: sfn.IStateMachine = new StateMachine(stack, 'StateMachine', { + definition: passTask, + stateMachineType: sfn.StateMachineType.EXPRESS, + }); + + return { stack, api, stateMachine }; +} + +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; +} + +describe('StepFunctions', () => { + test('minimal setup', () => { + //GIVEN + const { stack, api, stateMachine } = givenSetup(); + + //WHEN + const integ = new apigw.StepFunctionsIntegration(stateMachine); + api.root.addMethod('GET', integ); + + //THEN + expect(stack).toHaveResource('AWS::ApiGateway::Method', { + // HttpMethod: 'GET', + ResourceId: { + 'Fn::GetAtt': [ + 'myrestapiBAC2BF45', + 'RootResourceId', + ], + }, + RestApiId: { + Ref: 'myrestapiBAC2BF45', + }, + AuthorizationType: 'NONE', + Integration: { + IntegrationHttpMethod: 'POST', + IntegrationResponses: getIntegrationResponse(), + Type: 'AWS', + Uri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':states:action/StartSyncExecution', + ], + ], + }, + PassthroughBehavior: 'NEVER', + RequestTemplates: { + 'application/json': { + 'Fn::Join': [ + '', + [ + "\n #set($inputRoot = $input.path('$')) {\n \"input\": \"$util.escapeJavaScript($input.json('$'))\",\n \"stateMachineArn\": \"", + { + Ref: 'StateMachine2E01A3A5', + }, + '"\n }', + ], + ], + }, + }, + }, + }); + }); + + test('works for imported RestApi', () => { + const stack = new cdk.Stack(); + const api = apigw.RestApi.fromRestApiAttributes(stack, 'RestApi', { + restApiId: 'imported-rest-api-id', + rootResourceId: 'imported-root-resource-id', + }); + + const passTask = new sfn.Pass(stack, 'passTask', { + inputPath: '$.somekey', + }); + + const stateMachine: sfn.IStateMachine = new StateMachine(stack, 'StateMachine', { + definition: passTask, + stateMachineType: sfn.StateMachineType.EXPRESS, + }); + + api.root.addMethod('ANY', new apigw.StepFunctionsIntegration(stateMachine)); + + expect(stack).toHaveResource('AWS::ApiGateway::Method', { + HttpMethod: 'ANY', + ResourceId: 'imported-root-resource-id', + RestApiId: 'imported-rest-api-id', + AuthorizationType: 'NONE', + Integration: { + IntegrationHttpMethod: 'POST', + IntegrationResponses: [ + { + ResponseTemplates: { + 'application/json': "#set($inputRoot = $input.path('$'))\n #if($input.path('$.status').toString().equals(\"FAILED\"))\n #set($context.responseOverride.status = 500)\n { \n \"error\": \"$input.path('$.error')\",\n \"cause\": \"$input.path('$.cause')\"\n }\n #else\n $input.path('$.output')\n #end", + }, + StatusCode: '200', + }, + { + ResponseTemplates: { + 'application/json': '{\n "error": "Bad input!"\n }', + }, + SelectionPattern: '4\\d{2}', + StatusCode: '400', + }, + { + ResponseTemplates: { + 'application/json': "\"error\": $input.path('$.error')", + }, + SelectionPattern: '5\\d{2}', + StatusCode: '500', + }, + ], + PassthroughBehavior: 'NEVER', + RequestTemplates: { + 'application/json': { + 'Fn::Join': [ + '', + [ + "\n #set($inputRoot = $input.path('$')) {\n \"input\": \"$util.escapeJavaScript($input.json('$'))\",\n \"stateMachineArn\": \"", + { + Ref: 'StateMachine2E01A3A5', + }, + '"\n }', + ], + ], + }, + }, + Type: 'AWS', + Uri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':states:action/StartSyncExecution', + ], + ], + }, + }, + }); + }); + + test('fingerprint is not computed when stateMachineName is not specified', () => { + // GIVEN + const stack = new cdk.Stack(); + const restapi = new apigw.RestApi(stack, 'RestApi'); + const method = restapi.root.addMethod('ANY'); + + const passTask = new sfn.Pass(stack, 'passTask', { + inputPath: '$.somekey', + }); + + const stateMachine: sfn.IStateMachine = new StateMachine(stack, 'StateMachine', { + definition: passTask, + stateMachineType: sfn.StateMachineType.EXPRESS, + }); + + const integ = new apigw.StepFunctionsIntegration(stateMachine); + + // WHEN + const bindResult = integ.bind(method); + + // THEN + expect(bindResult?.deploymentToken).toBeUndefined(); + }); + + test('bind works for integration with imported State Machine', () => { + // GIVEN + const stack = new cdk.Stack(); + const restapi = new apigw.RestApi(stack, 'RestApi'); + const method = restapi.root.addMethod('ANY'); + const stateMachine: sfn.IStateMachine = StateMachine.fromStateMachineArn(stack, 'MyStateMachine', 'arn:aws:states:region:account:stateMachine:MyStateMachine'); + const integration = new apigw.StepFunctionsIntegration(stateMachine); + + // WHEN + const bindResult = integration.bind(method); + + // the deployment token should be defined since the function name + // should be a literal string. + expect(bindResult?.deploymentToken).toEqual('{"stateMachineName":"StateMachine-c8adc83b"}'); + }); +}); diff --git a/packages/@aws-cdk/aws-apigateway/test/stepFunctions-api.test.ts b/packages/@aws-cdk/aws-apigateway/test/stepFunctions-api.test.ts new file mode 100644 index 0000000000000..63295e81b5ce2 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/stepFunctions-api.test.ts @@ -0,0 +1,329 @@ +import '@aws-cdk/assert-internal/jest'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { StateMachine } from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import * as apigw from '../lib'; + +function givenSetup() { + const stack = new cdk.Stack(); + + const passTask = new sfn.Pass(stack, 'passTask', { + inputPath: '$.somekey', + }); + + const stateMachine: sfn.IStateMachine = new StateMachine(stack, 'StateMachine', { + definition: passTask, + stateMachineType: sfn.StateMachineType.EXPRESS, + }); + + return { stack, stateMachine }; +} + +function whenCondition(stack:cdk.Stack, stateMachine: sfn.IStateMachine) { + const api = new apigw.StepFunctionsRestApi(stack, 'StepFunctionsRestApi', { stateMachine: stateMachine }); + return api; +} + +function getMethodResponse() { + const methodResp = [ + { + StatusCode: '200', + ResponseModels: { + 'application/json': 'Empty', + }, + }, + { + StatusCode: '400', + ResponseModels: { + 'application/json': 'Error', + }, + }, + { + StatusCode: '500', + ResponseModels: { + 'application/json': 'Error', + }, + }, + ]; + + return methodResp; +} + +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; +} + +describe('Step Functions api', () => { + test('StepFunctionsRestApi defines correct REST API resouces', () => { + //GIVEN + const { stack, stateMachine } = givenSetup(); + + //WHEN + const api = whenCondition(stack, stateMachine); + + expect(() => { + api.root.addResource('not allowed'); + }).toThrow(); + + //THEN + expect(stack).toHaveResource('AWS::ApiGateway::Method', { + HttpMethod: 'ANY', + MethodResponses: getMethodResponse(), + AuthorizationType: 'NONE', + RestApiId: { + Ref: 'StepFunctionsRestApiC6E3E883', + }, + ResourceId: { + 'Fn::GetAtt': [ + 'StepFunctionsRestApiC6E3E883', + 'RootResourceId', + ], + }, + Integration: { + Credentials: { + 'Fn::GetAtt': [ + 'DefaultStateMachineapiRole1F29ACEB', + 'Arn', + ], + }, + IntegrationHttpMethod: 'POST', + IntegrationResponses: getIntegrationResponse(), + RequestTemplates: { + 'application/json': { + 'Fn::Join': [ + '', + [ + "\n #set($inputRoot = $input.path('$')) {\n \"input\": \"$util.escapeJavaScript($input.json('$'))\",\n \"stateMachineArn\": \"", + { + Ref: 'StateMachine2E01A3A5', + }, + '"\n }', + ], + ], + }, + }, + Type: 'AWS', + Uri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':states:action/StartSyncExecution', + ], + ], + }, + PassthroughBehavior: 'NEVER', + }, + }); + }); + + test('StepFunctionsRestApi defines correct REST API resouces with includeRequestContext set to true', () => { + //GIVEN + const { stack, stateMachine } = givenSetup(); + + //WHEN + const api = new apigw.StepFunctionsRestApi(stack, + 'StepFunctionsRestApi', { + stateMachine: stateMachine, + includeRequestContext: true, + }); + + expect(() => { + api.root.addResource('not allowed'); + }).toThrow(); + + //THEN + expect(stack).toHaveResource('AWS::ApiGateway::Method', { + HttpMethod: 'ANY', + MethodResponses: getMethodResponse(), + AuthorizationType: 'NONE', + RestApiId: { + Ref: 'StepFunctionsRestApiC6E3E883', + }, + ResourceId: { + 'Fn::GetAtt': [ + 'StepFunctionsRestApiC6E3E883', + 'RootResourceId', + ], + }, + Integration: { + Credentials: { + 'Fn::GetAtt': [ + 'DefaultStateMachineapiRole1F29ACEB', + 'Arn', + ], + }, + IntegrationHttpMethod: 'POST', + IntegrationResponses: getIntegrationResponse(), + RequestTemplates: { + 'application/json': { + 'Fn::Join': [ + '', + [ + "\n #set($allParams = $input.params())\n {\n \"input\": \"{\\\"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\\\"}}\",\n \"stateMachineArn\": \"", + { + Ref: 'StateMachine2E01A3A5', + }, + '"\n }', + ], + ], + }, + }, + Type: 'AWS', + Uri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':states:action/StartSyncExecution', + ], + ], + }, + PassthroughBehavior: 'NEVER', + }, + }); + }); + + + test('fails if options.defaultIntegration is set', () => { + //GIVEN + const { stack, stateMachine } = givenSetup(); + + const httpURL: string = 'https://foo/bar'; + + //WHEN & THEN + expect(() => new apigw.StepFunctionsRestApi(stack, 'StepFunctionsRestApi', { + stateMachine: stateMachine, + defaultIntegration: new apigw.HttpIntegration(httpURL), + })).toThrow(/Cannot specify \"defaultIntegration\" since Step Functions integration is automatically defined/); + + }); + + test('fails if State Machine is not of type EXPRESS', () => { + //GIVEN + const stack = new cdk.Stack(); + + const passTask = new sfn.Pass(stack, 'passTask', { + inputPath: '$.somekey', + }); + + const stateMachine: sfn.IStateMachine = new StateMachine(stack, 'StateMachine', { + definition: passTask, + stateMachineType: sfn.StateMachineType.STANDARD, + }); + + //WHEN & THEN + expect(() => new apigw.StepFunctionsRestApi(stack, 'StepFunctionsRestApi', { + stateMachine: stateMachine, + })).toThrow(/State Machine must be of type "EXPRESS". Please use StateMachineType.EXPRESS as the stateMachineType/); + + }); + + test('StepFunctionsRestApi defines a REST API with CORS enabled', () => { + const { stack, stateMachine } = givenSetup(); + + //WHEN + new apigw.StepFunctionsRestApi(stack, 'StepFunctionsRestApi', { + stateMachine: stateMachine, + defaultCorsPreflightOptions: { + allowOrigins: ['https://aws.amazon.com'], + allowMethods: ['GET', 'PUT'], + }, + }); + + //THEN + expect(stack).toHaveResource('AWS::ApiGateway::Method', { + HttpMethod: 'OPTIONS', + ResourceId: { + 'Fn::GetAtt': [ + 'StepFunctionsRestApiC6E3E883', + 'RootResourceId', + ], + }, + RestApiId: { + Ref: 'StepFunctionsRestApiC6E3E883', + }, + AuthorizationType: 'NONE', + Integration: { + IntegrationResponses: [ + { + ResponseParameters: { + 'method.response.header.Access-Control-Allow-Headers': "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + 'method.response.header.Access-Control-Allow-Origin': "'https://aws.amazon.com'", + 'method.response.header.Vary': "'Origin'", + 'method.response.header.Access-Control-Allow-Methods': "'GET,PUT'", + }, + StatusCode: '204', + }, + ], + RequestTemplates: { + 'application/json': '{ statusCode: 200 }', + }, + Type: 'MOCK', + }, + MethodResponses: [ + { + ResponseParameters: { + 'method.response.header.Access-Control-Allow-Headers': true, + 'method.response.header.Access-Control-Allow-Origin': true, + 'method.response.header.Vary': true, + 'method.response.header.Access-Control-Allow-Methods': true, + }, + StatusCode: '204', + }, + ], + }); + }); +}); From 23b6fd76e72f399bf39be81ada0acb1097ba819e Mon Sep 17 00:00:00 2001 From: Saqib Dhuka Date: Tue, 9 Nov 2021 17:45:26 +0000 Subject: [PATCH 02/35] Fixing request Context string build and template string vtl. Changed README and documentation. Making changes as per PR comments --- packages/@aws-cdk/aws-apigateway/README.md | 20 ++++--- .../integrations/execution-input-builder.ts | 54 ++++++++++--------- .../lib/integrations/stepfunctions.ts | 31 +++++++---- .../@aws-cdk/aws-apigateway/lib/restapi.ts | 1 + .../aws-apigateway/lib/stepfunctions-api.ts | 16 +++++- packages/@aws-cdk/aws-apigateway/package.json | 1 - ...entStack.ts => integ.stepfunctions-api.ts} | 8 +++ ...unctions.test.ts => stepfunctions.test.ts} | 0 ...-api.test.ts => stepfunctions-api.test.ts} | 0 9 files changed, 81 insertions(+), 50 deletions(-) rename packages/@aws-cdk/aws-apigateway/test/{integ.stepFunctions-api.deploymentStack.ts => integ.stepfunctions-api.ts} (79%) rename packages/@aws-cdk/aws-apigateway/test/integrations/{stepFunctions.test.ts => stepfunctions.test.ts} (100%) rename packages/@aws-cdk/aws-apigateway/test/{stepFunctions-api.test.ts => stepfunctions-api.test.ts} (100%) diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index efb40b782ed4d..8c3f9ea1b7a5b 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -111,24 +111,22 @@ item.addMethod('DELETE', new apigateway.HttpIntegration('http://amazon.com')); You can use Amazon API Gateway with AWS Step Functions as the backend integration, specifically Synchronous Express Workflows. -The `StepFunctionsRestApi` construct makes this easy and also sets up input, output and error mapping. The `StepFunctionsRestApi` construct sets up the API Gateway REST API with an `ANY` HTTP method and sets up the api role with the required permission to invoke `StartSyncExecution` action on the AWS StepFunctions state machine. This will enable you to invoke any one of the API Gateway HTTP methods and get a response from the backend AWS StepFunctions state machine. +The `StepFunctionsRestApi` construct makes this easy and also sets up input, output and error mapping. The `StepFunctionsRestApi` construct sets up the API Gateway REST API with an `ANY` HTTP method and sets up the api role with the required permission to invoke `StartSyncExecution` action on the AWS StepFunctions state machine. This will enable you to invoke any one of the API Gateway HTTP methods and get a response from the backend AWS StepFunctions state machine. Invoking any of the HTTP Method, using the example below, will return a "Hello!" message as the Response Body. The following code defines a REST API that routes all requests to the specified AWS StepFunctions state machine: ```ts -const stateMachine = new stepFunctions.StateMachine(this, 'StateMachine', ...); -new apigateway.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { - stateMachine: stateMachine, -}); -``` - -You can add requestContext (similar to input requestContext from lambda input) to the input. The 'requestContext' parameter includes account ID, user identity, etc. that can be used by customers that want to know the identity of authorized users on the state machine side. The following code defines a REST API like above but also adds 'requestContext' to the input of the State Machine: +const machineDefinition = new sfn.Pass(this, 'PassState', { + result: {value:"Hello!"}, +}) -```ts -const stateMachine = new stepFunctions.StateMachine(this, 'StateMachine', ...); +const stateMachine: sfn.IStateMachine = new sfn.StateMachine(this, 'StateMachine', { + definition: machineDefinition, + stateMachineType: sfn.StateMachineType.EXPRESS, +}); + new apigateway.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { stateMachine: stateMachine, - includeRequestContext: true, }); ``` diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/execution-input-builder.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/execution-input-builder.ts index aacda0821ae40..975f1f2e57313 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/execution-input-builder.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/execution-input-builder.ts @@ -109,21 +109,23 @@ export class ExecutionInputBuilder { constructor(input: string) { this._bodyStr = '"body": ' + input + ','; + this._contextStr = '"requestContext": {'; + this._endStr = '}'; } - /** - * set contextStr - * @param _context - * @returns ExecutionInputBuilder - */ - public withContext(_context: string | undefined): ExecutionInputBuilder { - if (_context == null) { - this._contextStr = ''; - return this; - } - this._contextStr = _context; - return this; - } + // /** + // * set contextStr + // * @param _context + // * @returns ExecutionInputBuilder + // */ + // public withContext(_context: string | undefined): ExecutionInputBuilder { + // if (_context == null) { + // this._contextStr = ''; + // return this; + // } + // this._contextStr = _context; + // return this; + // } /** * set accountIdStr @@ -378,19 +380,19 @@ export class ExecutionInputBuilder { return this; } - /** - * set _endStr - * @param _end - * @returns ExecutionInputBuilder - */ - public withEnd(_end: string | undefined): ExecutionInputBuilder { - if ( _end == null) { - this._endStr = ''; - return this; - } - this._endStr = _end; - return this; - } + // /** + // * set _endStr + // * @param _end + // * @returns ExecutionInputBuilder + // */ + // public withEnd(_end: string | undefined): ExecutionInputBuilder { + // if ( _end == null) { + // this._endStr = ''; + // return this; + // } + // this._endStr = _end; + // return this; + // } /** * returns _bodystr diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts index 4666978d32c4f..7bb9576d69056 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts @@ -17,8 +17,20 @@ export interface StepFunctionsIntegrationOptions extends IntegrationOptions { readonly corsEnabled?: boolean; /** - * Check if requestContext is enabled - * If enabled, requestContext is passed into the input of the State Machine. This requestContext is same as the lambda input (https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format) requestContext parameter. + * You can add requestContext (similar to input requestContext from lambda input) + * to the input. The 'requestContext' parameter includes account ID, user identity, etc. + * that can be used by customers that want to know the identity of authorized users on + * the state machine side. The following code defines a REST API like above but also + * adds 'requestContext' to the input of the State Machine: + * + * @example + * + * const stateMachine = new stepFunctions.StateMachine(this, 'StateMachine', ...); + * new apigateway.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { + * stateMachine: stateMachine, + * includeRequestContext: true, + * }); + * * @default false */ readonly includeRequestContext?: boolean; @@ -150,26 +162,26 @@ function templateString(stateMachine: sfn.IStateMachine, includeRequestContext: const search = '"'; const replaceWith = '\\"'; - if (typeof includeRequestContext === 'boolean' && includeRequestContext === true) { + const requestContextStrModified = requestContextStr.split(search).join(replaceWith); + if (includeRequestContext) { templateStr = ` #set($allParams = $input.params()) { - "input": "{${(requestContextStr.split(search).join(replaceWith))}}", + "input": "{${requestContextStrModified}}", "stateMachineArn": "${stateMachine.stateMachineArn}" }`; } else { templateStr = ` - #set($inputRoot = $input.path('$')) { - "input": "$util.escapeJavaScript($input.json('$'))", - "stateMachineArn": "${stateMachine.stateMachineArn}" - }`; + #set($inputRoot = $input.path('$')) { + "input": "$util.escapeJavaScript($input.json('$'))", + "stateMachineArn": "${stateMachine.stateMachineArn}" + }`; } return templateStr; } function requestContext(): string { const executionInput: ExecutionInput = new ExecutionInputBuilder('$util.escapeJavaScript($input.json(\'$\'))') - .withContext('"requestContext": {') .withAccountId('"$context.identity.accountId"') .withApiId('"$context.apiId"') .withApiKey('"$context.identity.apiKey"') @@ -188,7 +200,6 @@ function requestContext(): string { .withRequestId('"$context.requestId"') .withResourceId('"$context.resourceId"') .withResourcePath('"$context.resourcePath"') - .withEnd('}') .create(); return executionInput.retrieveAllAsString(); diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index e213ddad7f22f..aaad59e44c795 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -312,6 +312,7 @@ export abstract class RestApiBase extends Resource implements IRestApi { /** * A human friendly name for this Rest API. Note that this is different from `restApiId`. + * @attribute */ public readonly restApiName: string; diff --git a/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts b/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts index e6c3f0e196cfb..55216a3640d70 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts @@ -19,8 +19,20 @@ export interface StepFunctionsRestApiProps extends RestApiProps { readonly stateMachine: sfn.IStateMachine; /** - * Check if requestContext is enabled - * If enabled, requestContext is passed into the input of the State Machine. This requestContext is same as the lambda input (https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format) requestContext parameter. + * You can add requestContext (similar to input requestContext from lambda input) + * to the input. The 'requestContext' parameter includes account ID, user identity, etc. + * that can be used by customers that want to know the identity of authorized users on + * the state machine side. The following code defines a REST API like above but also + * adds 'requestContext' to the input of the State Machine: + * + * @example + * + * const stateMachine = new stepFunctions.StateMachine(this, 'StateMachine', ...); + * new apigateway.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { + * stateMachine: stateMachine, + * includeRequestContext: true, + * }); + * * @default false */ readonly includeRequestContext?: boolean; diff --git a/packages/@aws-cdk/aws-apigateway/package.json b/packages/@aws-cdk/aws-apigateway/package.json index 2b0793b3adf95..cf4ca6a4448bb 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -320,7 +320,6 @@ "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", diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepFunctions-api.deploymentStack.ts b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.ts similarity index 79% rename from packages/@aws-cdk/aws-apigateway/test/integ.stepFunctions-api.deploymentStack.ts rename to packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.ts index 14719e9ef901a..6363456a49e64 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.stepFunctions-api.deploymentStack.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.ts @@ -3,6 +3,13 @@ import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import * as apigw from '../lib'; +/** + * Stack verification steps: + * * `curl -X POST 'https://.execute-api..amazonaws.com/prod' \ + * * -d '{"key":"Hello"}' -H 'Content-Type: application/json'` + * The above should return a "Hello" response + */ + class StepFunctionsRestApiDeploymentStack extends cdk.Stack { constructor(scope: Construct) { super(scope, 'StepFunctionsRestApiDeploymentStack'); @@ -31,3 +38,4 @@ class StepFunctionsRestApiDeploymentStack extends cdk.Stack { const app = new cdk.App(); new StepFunctionsRestApiDeploymentStack(app); +app.synth(); diff --git a/packages/@aws-cdk/aws-apigateway/test/integrations/stepFunctions.test.ts b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts similarity index 100% rename from packages/@aws-cdk/aws-apigateway/test/integrations/stepFunctions.test.ts rename to packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts diff --git a/packages/@aws-cdk/aws-apigateway/test/stepFunctions-api.test.ts b/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts similarity index 100% rename from packages/@aws-cdk/aws-apigateway/test/stepFunctions-api.test.ts rename to packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts From 819468fb71216385ab05594df1c920c1fda299d0 Mon Sep 17 00:00:00 2001 From: Saqib Dhuka Date: Tue, 9 Nov 2021 21:45:03 +0000 Subject: [PATCH 03/35] Pull rebase --- .../integrations/execution-input-builder.ts | 28 ------------------- ... => integ.stepfunctions-api.expected.json} | 4 +-- .../test/integrations/stepfunctions.test.ts | 9 +++--- .../test/stepfunctions-api.test.ts | 4 +-- 4 files changed, 8 insertions(+), 37 deletions(-) rename packages/@aws-cdk/aws-apigateway/test/{integ.stepFunctions-api.deploymentStack.expected.json => integ.stepfunctions-api.expected.json} (97%) diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/execution-input-builder.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/execution-input-builder.ts index 975f1f2e57313..99600a7fd88b0 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/execution-input-builder.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/execution-input-builder.ts @@ -113,20 +113,6 @@ export class ExecutionInputBuilder { this._endStr = '}'; } - // /** - // * set contextStr - // * @param _context - // * @returns ExecutionInputBuilder - // */ - // public withContext(_context: string | undefined): ExecutionInputBuilder { - // if (_context == null) { - // this._contextStr = ''; - // return this; - // } - // this._contextStr = _context; - // return this; - // } - /** * set accountIdStr * @param _accountId @@ -380,20 +366,6 @@ export class ExecutionInputBuilder { return this; } - // /** - // * set _endStr - // * @param _end - // * @returns ExecutionInputBuilder - // */ - // public withEnd(_end: string | undefined): ExecutionInputBuilder { - // if ( _end == null) { - // this._endStr = ''; - // return this; - // } - // this._endStr = _end; - // return this; - // } - /** * returns _bodystr */ diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepFunctions-api.deploymentStack.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.expected.json similarity index 97% rename from packages/@aws-cdk/aws-apigateway/test/integ.stepFunctions-api.deploymentStack.expected.json rename to packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.expected.json index cc3ccb6c6484b..f77327e40f83a 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.stepFunctions-api.deploymentStack.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.expected.json @@ -185,11 +185,11 @@ "Fn::Join": [ "", [ - "\n #set($inputRoot = $input.path('$')) {\n \"input\": \"$util.escapeJavaScript($input.json('$'))\",\n \"stateMachineArn\": \"", + "\n #set($inputRoot = $input.path('$')) {\n \"input\": \"$util.escapeJavaScript($input.json('$'))\",\n \"stateMachineArn\": \"", { "Ref": "StateMachine2E01A3A5" }, - "\"\n }" + "\"\n }" ] ] } diff --git a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts index 98c01f97b60b7..bc2a35efc9976 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts @@ -72,7 +72,6 @@ describe('StepFunctions', () => { //THEN expect(stack).toHaveResource('AWS::ApiGateway::Method', { - // HttpMethod: 'GET', ResourceId: { 'Fn::GetAtt': [ 'myrestapiBAC2BF45', @@ -109,11 +108,11 @@ describe('StepFunctions', () => { 'Fn::Join': [ '', [ - "\n #set($inputRoot = $input.path('$')) {\n \"input\": \"$util.escapeJavaScript($input.json('$'))\",\n \"stateMachineArn\": \"", + "\n #set($inputRoot = $input.path('$')) {\n \"input\": \"$util.escapeJavaScript($input.json('$'))\",\n \"stateMachineArn\": \"", { Ref: 'StateMachine2E01A3A5', }, - '"\n }', + '"\n }', ], ], }, @@ -175,11 +174,11 @@ describe('StepFunctions', () => { 'Fn::Join': [ '', [ - "\n #set($inputRoot = $input.path('$')) {\n \"input\": \"$util.escapeJavaScript($input.json('$'))\",\n \"stateMachineArn\": \"", + "\n #set($inputRoot = $input.path('$')) {\n \"input\": \"$util.escapeJavaScript($input.json('$'))\",\n \"stateMachineArn\": \"", { Ref: 'StateMachine2E01A3A5', }, - '"\n }', + '"\n }', ], ], }, diff --git a/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts b/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts index 63295e81b5ce2..4cf14b384ce05 100644 --- a/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts @@ -131,11 +131,11 @@ describe('Step Functions api', () => { 'Fn::Join': [ '', [ - "\n #set($inputRoot = $input.path('$')) {\n \"input\": \"$util.escapeJavaScript($input.json('$'))\",\n \"stateMachineArn\": \"", + "\n #set($inputRoot = $input.path('$')) {\n \"input\": \"$util.escapeJavaScript($input.json('$'))\",\n \"stateMachineArn\": \"", { Ref: 'StateMachine2E01A3A5', }, - '"\n }', + '"\n }', ], ], }, From 128a4fa14930b5fe0d66bcaea39848f40187bdd4 Mon Sep 17 00:00:00 2001 From: Saqib Dhuka Date: Tue, 9 Nov 2021 17:45:26 +0000 Subject: [PATCH 04/35] Fixing request Context string build and template string vtl. Changed README and documentation. Making changes as per PR comments Changing imported case to be entire addr instead of 8 characters. --- .../@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts | 2 +- .../aws-apigateway/test/integrations/stepfunctions.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts index 7bb9576d69056..6312bbdd88428 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts @@ -79,7 +79,7 @@ export class StepFunctionsIntegration extends AwsIntegration { stateMachineName = (this.stateMachine.node.defaultChild as sfn.CfnStateMachine).stateMachineName; } else { //imported state machine - stateMachineName = 'StateMachine-' + (String(this.stateMachine.stack.node.addr).substring(0, 8)); + stateMachineName = 'StateMachine-' + (String(this.stateMachine.stack.node.addr)); } let deploymentToken; diff --git a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts index bc2a35efc9976..ee1ee31c1d06f 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts @@ -241,6 +241,6 @@ describe('StepFunctions', () => { // the deployment token should be defined since the function name // should be a literal string. - expect(bindResult?.deploymentToken).toEqual('{"stateMachineName":"StateMachine-c8adc83b"}'); + expect(bindResult?.deploymentToken).toEqual('{"stateMachineName":"StateMachine-c8adc83b19e793491b1c6ea0fd8b46cd9f32e592fc"}'); }); }); From a28fdab0a8d1b70809f78523806dc79588613713 Mon Sep 17 00:00:00 2001 From: Saqib Dhuka Date: Thu, 11 Nov 2021 01:36:47 +0000 Subject: [PATCH 05/35] Improving structure of the code as per PR comments. --- .../integrations/execution-input-builder.ts | 820 ------------------ .../aws-apigateway/lib/integrations/index.ts | 2 +- .../integrations/request-context-builder.ts | 163 ++++ .../lib/integrations/stepfunctions.ts | 113 ++- .../aws-apigateway/lib/stepfunctions-api.ts | 37 +- .../test/stepfunctions-api.test.ts | 21 +- 6 files changed, 286 insertions(+), 870 deletions(-) delete mode 100644 packages/@aws-cdk/aws-apigateway/lib/integrations/execution-input-builder.ts create mode 100644 packages/@aws-cdk/aws-apigateway/lib/integrations/request-context-builder.ts diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/execution-input-builder.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/execution-input-builder.ts deleted file mode 100644 index 99600a7fd88b0..0000000000000 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/execution-input-builder.ts +++ /dev/null @@ -1,820 +0,0 @@ -/** - * Builder to build execution input object - */ -export class ExecutionInputBuilder { - /** - * Input body string - */ - private _bodyStr: string; - - /** - * Context start string - */ - private _contextStr: string | undefined; - - /** - * Account ID string - */ - private _accountIdStr: string | undefined; - - /** - * Api ID string - */ - private _apiIdStr: string | undefined; - - /** - * Api Key string - */ - private _apiKeyStr: string | undefined; - - /** - * Authorizer Principal ID string - */ - private _authorizerPrincipalIdStr: string | undefined; - - /** - * Caller string - */ - private _callerStr: string | undefined; - - /** - * Cognito Authentication Provider string - */ - private _cognitoAuthenticationProviderStr: string | undefined; - - /** - * Cognito Authentication Type string - */ - private _cognitoAuthenticationTypeStr: string | undefined; - - /** - * Cognito Identity ID string - */ - private _cognitoIdentityIdStr: string | undefined; - - /** - * Cognito Identity Pool ID string - */ - private _cognitoIdentityPoolIdStr: string | undefined; - - /** - * Http Method string - */ - private _httpMethodStr: string | undefined; - - /** - * Stage string - */ - private _stageStr: string | undefined; - - /** - * Source IP string - */ - private _sourceIpStr: string | undefined; - - /** - * User string - */ - private _userStr: string | undefined; - - /** - * User Agent string - */ - private _userAgentStr: string | undefined; - - /** - * User Arn string - */ - private _userArnStr: string | undefined; - - /** - * Request ID string - */ - private _requestIdStr: string | undefined; - - /** - * Resource ID string - */ - private _resourceIdStr: string | undefined; - - /** - * Resource Path string - */ - private _resourcePathStr: string | undefined; - - /** - * End string - */ - private _endStr: string | undefined; - - constructor(input: string) { - this._bodyStr = '"body": ' + input + ','; - this._contextStr = '"requestContext": {'; - this._endStr = '}'; - } - - /** - * set accountIdStr - * @param _accountId - * @returns ExecutionInputBuilder - */ - public withAccountId(_accountId: string | undefined): ExecutionInputBuilder { - if (_accountId == null) { - this._accountIdStr = ''; - return this; - } - this._accountIdStr = '"accountId":' + _accountId + ','; - return this; - } - - /** - * set apiIdStr - * @param _apiId - * @returns ExecutionInputBuilder - */ - public withApiId(_apiId: string | undefined): ExecutionInputBuilder { - if (_apiId == null) { - this._apiIdStr = ''; - return this; - } - this._apiIdStr = '"apiId":' + _apiId + ','; - return this; - } - - /** - * set apiKeyStr - * @param _apiKey - * @returns ExecutionInputBuilder - */ - public withApiKey(_apiKey: string | undefined): ExecutionInputBuilder { - if (_apiKey == null) { - this._apiKeyStr = ''; - return this; - } - this._apiKeyStr = '"apiKey":' + _apiKey + ','; - return this; - } - - /** - * set _authorizerPrincipalIdStr - * @param _authorizerPrincipalId - * @returns ExecutionInputBuilder - */ - public withAuthorizerPrincipalId(_authorizerPrincipalId: string | undefined): ExecutionInputBuilder { - if (_authorizerPrincipalId == null) { - this._authorizerPrincipalIdStr = ''; - return this; - } - this._authorizerPrincipalIdStr = '"authorizerPrincipalId":' + _authorizerPrincipalId + ','; - return this; - } - - /** - * set _callerStr - * @param _caller - * @returns ExecutionInputBuilder - */ - public withCaller(_caller: string | undefined): ExecutionInputBuilder { - if (_caller == null) { - this._callerStr = ''; - return this; - } - this._callerStr = '"caller":' + _caller + ','; - return this; - } - - /** - * set _cognitoAuthenticationProviderStr - * @param _cognitoAuthenticationProvider - * @returns ExecutionInputBuilder - */ - public withCognitoAuthenticationProvider(_cognitoAuthenticationProvider: string | undefined): ExecutionInputBuilder { - if (_cognitoAuthenticationProvider == null) { - this._cognitoAuthenticationProviderStr = ''; - return this; - } - this._cognitoAuthenticationProviderStr = '"cognitoAuthenticationProvider":' + _cognitoAuthenticationProvider + ','; - return this; - } - - /** - * set _cognitoAuthenticationTypeStr - * @param _cognitoAuthenticationType - * @returns ExecutionInputBuilder - */ - public withCognitoAuthenticationType(_cognitoAuthenticationType: string | undefined): ExecutionInputBuilder { - if (_cognitoAuthenticationType == null) { - this._cognitoAuthenticationTypeStr = ''; - return this; - } - this._cognitoAuthenticationTypeStr = '"cognitoAuthenticationType":' + _cognitoAuthenticationType + ','; - return this; - } - - /** - * set _cognitoIdentityIdStr - * @param _cognitoIdentityId - * @returns ExecutionInputBuilder - */ - public withCognitoIdentityId(_cognitoIdentityId: string | undefined): ExecutionInputBuilder { - if (_cognitoIdentityId == null) { - this._cognitoIdentityIdStr = ''; - return this; - } - this._cognitoIdentityIdStr = '"cognitoIdentityId":' + _cognitoIdentityId + ','; - return this; - } - - /** - * set _cognitoIdentityPoolIdStr - * @param _cognitoIdentityPoolId - * @returns ExecutionInputBuilder - */ - public withCognitoIdentityPoolId(_cognitoIdentityPoolId: string | undefined): ExecutionInputBuilder { - if (_cognitoIdentityPoolId == null) { - this._cognitoIdentityPoolIdStr = ''; - return this; - } - this._cognitoIdentityPoolIdStr = '"cognitoIdentityPoolId":' + _cognitoIdentityPoolId + ','; - return this; - } - - /** - * set _httpMethodStr - * @param _httpMethod - * @returns ExecutionInputBuilder - */ - public withHttpMethod(_httpMethod: string | undefined): ExecutionInputBuilder { - if (_httpMethod == null) { - this._httpMethodStr = ''; - return this; - } - this._httpMethodStr = '"httpMethod":' + _httpMethod + ','; - return this; - } - - /** - * set _stageStr - * @param _stage - * @returns ExecutionInputBuilder - */ - public withStage(_stage: string | undefined): ExecutionInputBuilder { - if (_stage == null) { - this._stageStr = ''; - return this; - } - this._stageStr = '"stage":' + _stage + ','; - return this; - } - - /** - * set _sourceIpStr - * @param _sourceIp - * @returns ExecutionInputBuilder - */ - public withSourceIp(_sourceIp: string | undefined): ExecutionInputBuilder { - if (_sourceIp == null) { - this._sourceIpStr = ''; - return this; - } - this._sourceIpStr = '"sourceIp":' + _sourceIp + ','; - return this; - } - - /** - * set _userStr - * @param _user - * @returns ExecutionInputBuilder - */ - public withUser(_user: string | undefined): ExecutionInputBuilder { - if (_user == null) { - this._userStr = ''; - return this; - } - this._userStr = '"user":' + _user + ','; - return this; - } - - /** - * set _userAgentStr - * @param _userAgent - * @returns ExecutionInputBuilder - */ - - public withUserAgent(_userAgent: string | undefined): ExecutionInputBuilder { - if (_userAgent == null) { - this._userAgentStr = ''; - return this; - } - this._userAgentStr = '"userAgent":' + _userAgent + ','; - return this; - } - - /** - * set _userArnStr - * @param _userArn - * @returns ExecutionInputBuilder - */ - public withUserArn(_userArn: string | undefined): ExecutionInputBuilder { - if (_userArn == null) { - this._userArnStr = ''; - return this; - } - this._userArnStr = '"userArn":' + _userArn + ','; - return this; - } - - /** - * set _requestIdStr - * @param _requestId - * @returns ExecutionInputBuilder - */ - public withRequestId(_requestId: string | undefined): ExecutionInputBuilder { - if (_requestId == null) { - this._requestIdStr = ''; - return this; - } - this._requestIdStr = '"requestId":' + _requestId + ','; - return this; - } - - /** - * set _resourceIdStr - * @param _resourceId - * @returns ExecutionInputBuilder - */ - public withResourceId(_resourceId: string | undefined): ExecutionInputBuilder { - if (_resourceId == null) { - this._resourceIdStr = ''; - return this; - } - this._resourceIdStr = '"resourceId":' + _resourceId + ','; - return this; - } - - /** - * set _resourcePathStr - * @param _resourcePath - * @returns ExecutionInputBuilder - */ - public withResourcePath(_resourcePath: string | undefined): ExecutionInputBuilder { - if (_resourcePath == null) { - this._resourcePathStr = ''; - return this; - } - this._resourcePathStr = '"resourcePath":' + _resourcePath; - return this; - } - - /** - * returns _bodystr - */ - public get retrieveBodyStr() { - return this._bodyStr; - } - - /** - * returns _contextStr - */ - public get retrieveContextStr() { - return this._contextStr; - } - - /** - * returns _accountIdStr - */ - public get retrieveAccountIdStr() { - return this._accountIdStr; - } - - /** - * returns _apiIdStr - */ - public get retrieveApiIdStr() { - return this._apiIdStr; - } - - /** - * returns _apiKeyStr - */ - public get retrieveApiKeyStr() { - return this._apiKeyStr; - } - - /** - * returns _authorizerPrincipalIdStr - */ - public get retrieveAuthorizerPrincipalIdStr() { - return this._authorizerPrincipalIdStr; - } - - /** - * returns _callerStr - */ - public get retrieveCallerStr() { - return this._callerStr; - } - - /** - * returns _cognitoAuthenticationProviderStr - */ - public get retrieveCognitoAuthenticationProviderStr() { - return this._cognitoAuthenticationProviderStr; - } - - /** - * returns _cognitoAuthenticationTypeStr - */ - public get retrieveCognitoAuthenticationTypeStr() { - return this._cognitoAuthenticationTypeStr; - } - - /** - * returns _cognitoIdentityIdStr - */ - public get retrieveCognitoIdentityIdStr() { - return this._cognitoIdentityIdStr; - } - - /** - * returns _cognitoIdentityPoolIdStr - */ - public get retrieveCognitoIdentityPoolIdStr() { - return this._cognitoIdentityPoolIdStr; - } - - /** - * returns _httpMethodStr - */ - public get retrieveHttpMethodStr() { - return this._httpMethodStr; - } - - /** - * returns _stageStr - */ - public get retrieveStageStr() { - return this._stageStr; - } - - /** - * returns _sourceIpStr - */ - public get retrieveSourceIpStr() { - return this._sourceIpStr; - } - - /** - * returns _userStr - */ - public get retrieveUserStr() { - return this._userStr; - } - - /** - * returns _userAgentStr - */ - public get retrieveUserAgentStr() { - return this._userAgentStr; - } - - /** - * returns _userArnStr - */ - public get retrieveUserArnStr() { - return this._userArnStr ; - } - - /** - * returns _requestIdStr - */ - public get retrieveRequestIdStr() { - return this._requestIdStr; - } - - /** - * returns _resourceIdStr - */ - public get retrieveResourceIdStr() { - return this._resourceIdStr; - } - - /** - * returns _resourcePathStr - */ - public get retrieveResourcePathStr() { - return this._resourcePathStr; - } - - /** - * returns _endstr - */ - public get retrieveEndStr() { - return this._endStr; - } - - /** - * Returns object - * @returns ExecutionInput - */ - public create(): ExecutionInput { - return new ExecutionInput(this); - } - -} - -/** - * execution input object - */ -export class ExecutionInput { - /** - * Input body string - */ - private _bodyStr: string; - - /** - * Context start string - */ - private _contextStr: string | undefined; - - /** - * Account ID string - */ - private _accountIdStr: string | undefined; - - /** - * Api ID string - */ - private _apiIdStr: string | undefined; - - /** - * Api Key string - */ - private _apiKeyStr: string | undefined; - - /** - * Authorizer Principal ID string - */ - private _authorizerPrincipalIdStr: string | undefined; - - /** - * Caller string - */ - private _callerStr: string | undefined; - - /** - * Cognito Authentication Provider string - */ - private _cognitoAuthenticationProviderStr: string | undefined; - - /** - * Cognito Authentication Type string - */ - private _cognitoAuthenticationTypeStr: string | undefined; - - /** - * Cognito Identity ID string - */ - private _cognitoIdentityIdStr: string | undefined; - - /** - * Cognito Identity Pool ID string - */ - private _cognitoIdentityPoolIdStr: string | undefined; - - /** - * Http Method string - */ - private _httpMethodStr: string | undefined; - - /** - * Stage string - */ - private _stageStr: string | undefined; - - /** - * Source IP string - */ - private _sourceIpStr: string | undefined; - - /** - * User string - */ - private _userStr: string | undefined; - - /** - * User Agent string - */ - private _userAgentStr: string | undefined; - - /** - * User Arn string - */ - private _userArnStr: string | undefined; - - /** - * Request ID string - */ - private _requestIdStr: string | undefined; - - /** - * Resource ID string - */ - private _resourceIdStr: string | undefined; - - /** - * Resource Path string - */ - private _resourcePathStr: string | undefined; - - /** - * End string - */ - private _endStr: string | undefined; - - constructor(builder: ExecutionInputBuilder) { - this._bodyStr = builder.retrieveBodyStr; - this._contextStr = builder.retrieveContextStr; - this._accountIdStr = builder.retrieveAccountIdStr; - this._apiIdStr = builder.retrieveApiIdStr; - this._apiKeyStr = builder.retrieveApiKeyStr; - this._authorizerPrincipalIdStr = builder.retrieveAuthorizerPrincipalIdStr; - this._callerStr = builder.retrieveCallerStr; - this._cognitoAuthenticationProviderStr = builder.retrieveCognitoAuthenticationProviderStr; - this._cognitoAuthenticationTypeStr = builder.retrieveCognitoAuthenticationTypeStr; - this._cognitoIdentityIdStr = builder.retrieveCognitoIdentityIdStr; - this._cognitoIdentityPoolIdStr = builder.retrieveCognitoIdentityPoolIdStr; - this._httpMethodStr = builder.retrieveHttpMethodStr; - this._stageStr = builder.retrieveStageStr; - this._sourceIpStr = builder.retrieveSourceIpStr; - this._userStr = builder.retrieveUserStr; - this._userAgentStr = builder.retrieveUserAgentStr; - this._userArnStr = builder.retrieveUserArnStr; - this._requestIdStr = builder.retrieveRequestIdStr; - this._resourceIdStr = builder.retrieveResourceIdStr; - this._resourcePathStr = builder.retrieveResourcePathStr; - this._endStr = builder.retrieveEndStr; - } - - /** - * returns _bodystr - */ - public get retrieveBodyStr() { - return this._bodyStr; - } - - /** - * returns _contextStr - */ - public get retrieveContextStr() { - return this._contextStr; - } - - /** - * returns _accountIdStr - */ - public get retrieveAccountIdStr() { - return this._accountIdStr; - } - - /** - * returns _apiIdStr - */ - public get retrieveApiIdStr() { - return this._apiIdStr; - } - - /** - * returns _apiKeyStr - */ - public get retrieveApiKeyStr() { - return this._apiKeyStr; - } - - /** - * returns _authorizerPrincipalIdStr - */ - public get retrieveAuthorizerPrincipalIdStr() { - return this._authorizerPrincipalIdStr; - } - - /** - * returns _callerStr - */ - public get retrieveCallerStr() { - return this._callerStr; - } - - /** - * returns _cognitoAuthenticationProviderStr - */ - public get retrieveCognitoAuthenticationProviderStr() { - return this._cognitoAuthenticationProviderStr; - } - - /** - * returns _cognitoAuthenticationTypeStr - */ - public get retrieveCognitoAuthenticationTypeStr() { - return this._cognitoAuthenticationTypeStr; - } - - /** - * returns _cognitoIdentityIdStr - */ - public get retrieveCognitoIdentityIdStr() { - return this._cognitoIdentityIdStr; - } - - /** - * returns _cognitoIdentityPoolIdStr - */ - public get retrieveCognitoIdentityPoolIdStr() { - return this._cognitoIdentityPoolIdStr; - } - - /** - * returns _httpMethodStr - */ - public get retrieveHttpMethodStr() { - return this._httpMethodStr; - } - - /** - * returns _stageStr - */ - public get retrieveStageStr() { - return this._stageStr; - } - - /** - * returns _sourceIpStr - */ - public get retrieveSourceIpStr() { - return this._sourceIpStr; - } - - /** - * returns _userStr - */ - public get retrieveUserStr() { - return this._userStr; - } - - /** - * returns _userAgentStr - */ - public get retrieveUserAgentStr() { - return this._userAgentStr; - } - - /** - * returns _userArnStr - */ - public get retrieveUserArnStr() { - return this._userArnStr ; - } - - /** - * returns _requestIdStr - */ - public get retrieveRequestIdStr() { - return this._requestIdStr; - } - - /** - * returns _resourceIdStr - */ - public get retrieveResourceIdStr() { - return this._resourceIdStr; - } - - /** - * returns _resourcePathStr - */ - public get retrieveResourcePathStr() { - return this._resourcePathStr; - } - - /** - * returns _endstr - */ - public get retrieveEndStr() { - return this._endStr; - } - - /** - * Returns all properties as a single single - */ - public retrieveAllAsString(): string { - const executionInputStr :string = this.retrieveBodyStr + this.retrieveContextStr + this.retrieveAccountIdStr + - this.retrieveApiIdStr + this.retrieveApiKeyStr + this.retrieveAuthorizerPrincipalIdStr + - this.retrieveCallerStr + this.retrieveCognitoAuthenticationProviderStr + - this.retrieveCognitoAuthenticationTypeStr + this.retrieveCognitoIdentityIdStr + - this.retrieveCognitoIdentityPoolIdStr + this.retrieveHttpMethodStr + - this.retrieveStageStr + this.retrieveSourceIpStr + this.retrieveUserStr + this.retrieveUserAgentStr + - this.retrieveUserArnStr + this.retrieveRequestIdStr + this.retrieveResourceIdStr + this.retrieveResourcePathStr + this.retrieveEndStr; - - return executionInputStr; - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/index.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/index.ts index a1590114a131f..f1fe3526fc324 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/index.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/index.ts @@ -3,4 +3,4 @@ export * from './lambda'; export * from './http'; export * from './mock'; export * from './stepfunctions'; -export * from './execution-input-builder'; +export * from './request-context-builder'; diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/request-context-builder.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/request-context-builder.ts new file mode 100644 index 0000000000000..0c3def62743ba --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/request-context-builder.ts @@ -0,0 +1,163 @@ +/** + * Request Context interface + */ +export interface RequestContext { + /** + * Account ID string + * @default false + */ + readonly accountId?: boolean; + + /** + * Api ID string + * @default false + */ + readonly apiId?: boolean; + + /** + * Api Key string + * @default false + */ + readonly apiKey?: boolean; + + /** + * Authorizer Principal ID string + * @default false + */ + readonly authorizerPrincipalId?: boolean; + + /** + * Caller string + * @default false + */ + readonly caller?: boolean; + + /** + * Cognito Authentication Provider string + * @default false + */ + readonly cognitoAuthenticationProvider?: boolean; + + /** + * Cognito Authentication Type string + * @default false + */ + readonly cognitoAuthenticationType?: boolean; + + /** + * Cognito Identity ID string + * @default false + */ + readonly cognitoIdentityId?: boolean; + + /** + * Cognito Identity Pool ID string + * @default false + */ + readonly cognitoIdentityPoolId?: boolean; + + /** + * Http Method string + * @default false + */ + readonly httpMethod?: boolean; + + /** + * Stage string + * @default false + */ + readonly stage?: boolean; + + /** + * Source IP string + * @default false + */ + readonly sourceIp?: boolean; + + /** + * User string + * @default false + */ + readonly user?: boolean; + + /** + * User Agent string + * @default false + */ + readonly userAgent?: boolean; + + /** + * User Arn string + * @default false + */ + readonly userArn?: boolean; + + /** + * Request ID string + * @default false + */ + readonly requestId?: boolean; + + /** + * Resource ID string + * @default false + */ + readonly resourceId?: boolean; + + /** + * Resource Path string + * @default false + */ + readonly resourcePath?: boolean; +} + +/** + * Builder to build Request Context object + */ +export class RequestContextBuilder { + + constructor() {} + + /** + * Returns the requestContext String + * @param requestContextInterface + * @returns requestContextString + */ + public with(requestContextInterface:RequestContext): string { + const bodyStr: string = '"body": $util.escapeJavaScript($input.json(\'$\')),'; + const contextStr: string = '"requestContext": {'; + const accountIdStr: string = (requestContextInterface.accountId) ? '"accountId":"$context.identity.accountId",' : ''; + const apiIdStr: string = (requestContextInterface.apiId) ? '"apiId":"$context.apiId",' : ''; + const apiKeyStr: string = (requestContextInterface.apiKey) ? '"apiKey":"$context.identity.apiKey",' : ''; + const authorizerPrincipalIdStr: string = (requestContextInterface.authorizerPrincipalId) ? '"authorizerPrincipalId":"$context.authorizer.principalId",' : ''; + const callerStr: string = (requestContextInterface.caller) ? '"caller":"$context.identity.caller",' : ''; + const cognitoAuthenticationProviderStr: string = (requestContextInterface.cognitoAuthenticationProvider) ? '"cognitoAuthenticationProvider":"$context.identity.cognitoAuthenticationProvider",' : ''; + const cognitoAuthenticationTypeStr: string = (requestContextInterface.cognitoAuthenticationType) ? '"cognitoAuthenticationType":"$context.identity.cognitoAuthenticationType",' : ''; + const cognitoIdentityIdStr: string = (requestContextInterface.cognitoIdentityId) ? '"cognitoIdentityId":"$context.identity.cognitoIdentityId",' : ''; + const cognitoIdentityPoolIdStr: string = (requestContextInterface.cognitoIdentityPoolId) ? '"cognitoIdentityPoolId":"$context.identity.cognitoIdentityPoolId",' : ''; + const httpMethodStr: string = (requestContextInterface.httpMethod) ? '"httpMethod":"$context.httpMethod",' : ''; + const stageStr: string = (requestContextInterface.stage) ? '"stage":"$context.stage",' : ''; + const sourceIpStr: string = (requestContextInterface.sourceIp) ? '"sourceIp":"$context.identity.sourceIp",' : ''; + const userStr: string = (requestContextInterface.user) ? '"user":"$context.identity.user",' : ''; + const userAgentStr: string = (requestContextInterface.userAgent) ? '"userAgent":"$context.identity.userAgent",' : ''; + const userArnStr: string = (requestContextInterface.userArn) ? '"userArn":"$context.identity.userArn",' : ''; + const requestIdStr: string = (requestContextInterface.requestId) ? '"requestId":"$context.requestId",' : ''; + const resourceIdStr: string = (requestContextInterface.resourceId) ? '"resourceId":"$context.resourceId",' : ''; + const resourcePathStr: string = (requestContextInterface.resourcePath) ? '"resourcePath":"$context.resourcePath",' : ''; + const endStr = '}'; + + let requestContextString :string = bodyStr + contextStr + accountIdStr + apiIdStr + apiKeyStr + + authorizerPrincipalIdStr + callerStr + cognitoAuthenticationProviderStr + cognitoAuthenticationTypeStr + + cognitoIdentityIdStr + cognitoIdentityPoolIdStr + httpMethodStr + stageStr + sourceIpStr + userStr + + userAgentStr + userArnStr + requestIdStr + resourceIdStr + resourcePathStr + endStr; + + if (requestContextString !== (bodyStr + contextStr + endStr)) { + //Removing the last comma froom the string only if it has a value inside + requestContextString = requestContextString.substring(0, requestContextString.length-2) + '}'; + } + + return requestContextString; + } + + +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts index 6312bbdd88428..0a0ed82ac1c91 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts @@ -1,7 +1,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import { Token } from '@aws-cdk/core'; -import { ExecutionInput, ExecutionInputBuilder } from '.'; +import { RequestContextBuilder, RequestContext } from '.'; import { IntegrationConfig, IntegrationOptions, PassthroughBehavior } from '../integration'; import { Method } from '../method'; import { AwsIntegration } from './aws'; @@ -10,6 +10,13 @@ import { AwsIntegration } from './aws'; * Options when configuring Step Functions integration with Rest API */ export interface StepFunctionsIntegrationOptions extends IntegrationOptions { + /** + * Action for the Step Functions integration. The list of supported API actions can be found + * on https://docs.aws.amazon.com/step-functions/latest/apireference/API_Operations.html + * @default 'StartSyncExecution' + */ + readonly action: string; + /** * Check if cors is enabled * @default false @@ -20,20 +27,41 @@ export interface StepFunctionsIntegrationOptions extends IntegrationOptions { * You can add requestContext (similar to input requestContext from lambda input) * to the input. The 'requestContext' parameter includes account ID, user identity, etc. * that can be used by customers that want to know the identity of authorized users on - * the state machine side. The following code defines a REST API like above but also - * adds 'requestContext' to the input of the State Machine: + * the state machine side. You can individually select the keys you want by setting them to true. + * The following code defines a REST API like above but also adds 'requestContext' to the input + * of the State Machine: * * @example * * const stateMachine = new stepFunctions.StateMachine(this, 'StateMachine', ...); * new apigateway.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { * stateMachine: stateMachine, - * includeRequestContext: true, + * requestContext: { + * accountId: true, + * apiId: true, + * apiKey: true, + * authorizerPrincipalId: true, + * caller: true, + * cognitoAuthenticationProvider: true, + * cognitoAuthenticationType: true, + * cognitoIdentityId: true, + * cognitoIdentityPoolId: true, + * httpMethod: true, + * stage: true, + * sourceIp: true, + * user: true, + * userAgent: true, + * userArn: true, + * requestId: true, + * resourceId: true, + * resourcePath: true, + * }, * }); * - * @default false + * @default - all parameters within request context will be set as false */ - readonly includeRequestContext?: boolean; + readonly requestContext?: RequestContext; + } /** @@ -46,14 +74,15 @@ export interface StepFunctionsIntegrationOptions extends IntegrationOptions { */ export class StepFunctionsIntegration extends AwsIntegration { private readonly stateMachine: sfn.IStateMachine; - - constructor(stateMachine: sfn.IStateMachine, options: StepFunctionsIntegrationOptions = { }) { + private readonly action: string; + constructor(stateMachine: sfn.IStateMachine, options: StepFunctionsIntegrationOptions = { action: 'StartSyncExecution' }) { + let requestContextRequested: boolean = (options.requestContext) ? true: false; const integResponse = integrationResponse(); - const requestTemplate = requestTemplates(stateMachine, options.includeRequestContext); + const requestTemplate = requestTemplates(stateMachine, requestContextRequested, options.requestContext); super({ service: 'states', - action: 'StartSyncExecution', + action: options.action, options: { credentialsRole: options.credentialsRole, integrationResponses: integResponse, @@ -63,13 +92,14 @@ export class StepFunctionsIntegration extends AwsIntegration { }); this.stateMachine = stateMachine; + this.action = options.action; } public bind(method: Method): IntegrationConfig { const bindResult = super.bind(method); const principal = new iam.ServicePrincipal('apigateway.amazonaws.com'); - this.stateMachine.grantExecution(principal, 'states:StartSyncExecution'); + this.stateMachine.grantExecution(principal, `states:${this.action}`); let stateMachineName; @@ -145,8 +175,8 @@ function integrationResponse() { return integResponse; } -function requestTemplates(stateMachine: sfn.IStateMachine, includeRequestContext: boolean | undefined) { - const templateStr = templateString(stateMachine, includeRequestContext); +function requestTemplates(stateMachine: sfn.IStateMachine, includeRequestContext: boolean, requestContextObj: RequestContext | undefined) { + const templateStr = templateString(stateMachine, includeRequestContext, requestContextObj); const requestTemplate: { [contentType:string] : string } = { @@ -156,14 +186,15 @@ function requestTemplates(stateMachine: sfn.IStateMachine, includeRequestContext return requestTemplate; } -function templateString(stateMachine: sfn.IStateMachine, includeRequestContext: boolean | undefined): string { +function templateString(stateMachine: sfn.IStateMachine, includeRequestContext: boolean, requestContextObj: RequestContext | undefined): string { let templateStr: string; - const requestContextStr = requestContext(); - const search = '"'; - const replaceWith = '\\"'; - const requestContextStrModified = requestContextStr.split(search).join(replaceWith); if (includeRequestContext) { + const requestContextStr = requestContext(requestContextObj); + + const search = '"'; + const replaceWith = '\\"'; + const requestContextStrModified = requestContextStr.split(search).join(replaceWith); templateStr = ` #set($allParams = $input.params()) { @@ -180,27 +211,27 @@ function templateString(stateMachine: sfn.IStateMachine, includeRequestContext: return templateStr; } -function requestContext(): string { - const executionInput: ExecutionInput = new ExecutionInputBuilder('$util.escapeJavaScript($input.json(\'$\'))') - .withAccountId('"$context.identity.accountId"') - .withApiId('"$context.apiId"') - .withApiKey('"$context.identity.apiKey"') - .withAuthorizerPrincipalId('"$context.authorizer.principalId"') - .withCaller('"$context.identity.caller"') - .withCognitoAuthenticationProvider('"$context.identity.cognitoAuthenticationProvider"') - .withCognitoAuthenticationType('"$context.identity.cognitoAuthenticationType"') - .withCognitoIdentityId('"$context.identity.cognitoIdentityId"') - .withCognitoIdentityPoolId('"$context.identity.cognitoIdentityPoolId"') - .withHttpMethod('"$context.httpMethod"') - .withStage('"$context.stage"') - .withSourceIp('"$context.identity.sourceIp"') - .withUser('"$context.identity.user"') - .withUserAgent('"$context.identity.userAgent"') - .withUserArn('"$context.identity.userArn"') - .withRequestId('"$context.requestId"') - .withResourceId('"$context.resourceId"') - .withResourcePath('"$context.resourcePath"') - .create(); - - return executionInput.retrieveAllAsString(); +function requestContext(requestContextObj: RequestContext | undefined): string { + const requestContextStr: string = new RequestContextBuilder().with({ + accountId: (requestContextObj) ? requestContextObj?.accountId : false, + apiId: (requestContextObj) ? requestContextObj?.apiId : false, + apiKey: (requestContextObj) ? requestContextObj?.apiKey : false, + authorizerPrincipalId: (requestContextObj) ? requestContextObj?.authorizerPrincipalId : false, + caller: (requestContextObj) ? requestContextObj?.caller : false, + cognitoAuthenticationProvider: (requestContextObj) ? requestContextObj?.cognitoAuthenticationProvider : false, + cognitoAuthenticationType: (requestContextObj) ? requestContextObj?.cognitoAuthenticationType : false, + cognitoIdentityId: (requestContextObj) ? requestContextObj?.cognitoIdentityId : false, + cognitoIdentityPoolId: (requestContextObj) ? requestContextObj?.cognitoIdentityPoolId : false, + httpMethod: (requestContextObj) ? requestContextObj?.httpMethod : false, + stage: (requestContextObj) ? requestContextObj?.stage : false, + sourceIp: (requestContextObj) ? requestContextObj?.sourceIp : false, + user: (requestContextObj) ? requestContextObj?.user : false, + userAgent: (requestContextObj) ? requestContextObj?.userAgent : false, + userArn: (requestContextObj) ? requestContextObj?.userArn : false, + requestId: (requestContextObj) ? requestContextObj?.requestId : false, + resourceId: (requestContextObj) ? requestContextObj?.resourceId : false, + resourcePath: (requestContextObj) ? requestContextObj?.resourcePath : false, + }); + + return requestContextStr; } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts b/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts index 55216a3640d70..20837dd95ee08 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts @@ -2,6 +2,7 @@ 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 { RequestContext } from './integrations'; import { StepFunctionsIntegration } from './integrations/stepfunctions'; import { Model } from './model'; @@ -22,20 +23,40 @@ export interface StepFunctionsRestApiProps extends RestApiProps { * You can add requestContext (similar to input requestContext from lambda input) * to the input. The 'requestContext' parameter includes account ID, user identity, etc. * that can be used by customers that want to know the identity of authorized users on - * the state machine side. The following code defines a REST API like above but also - * adds 'requestContext' to the input of the State Machine: + * the state machine side. You can individually select the keys you want by setting them to true. + * The following code defines a REST API like above but also adds 'requestContext' to the input + * of the State Machine: * * @example * * const stateMachine = new stepFunctions.StateMachine(this, 'StateMachine', ...); * new apigateway.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { * stateMachine: stateMachine, - * includeRequestContext: true, + * requestContext: { + * accountId: true, + * apiId: true, + * apiKey: true, + * authorizerPrincipalId: true, + * caller: true, + * cognitoAuthenticationProvider: true, + * cognitoAuthenticationType: true, + * cognitoIdentityId: true, + * cognitoIdentityPoolId: true, + * httpMethod: true, + * stage: true, + * sourceIp: true, + * user: true, + * userAgent: true, + * userArn: true, + * requestId: true, + * resourceId: true, + * resourcePath: true, + * }, * }); * - * @default false + * @default - all parameters within request context will be set as false */ - readonly includeRequestContext?: boolean; + readonly requestContext?: RequestContext; } /** @@ -66,7 +87,8 @@ export class StepFunctionsRestApi extends RestApi { defaultIntegration: new StepFunctionsIntegration(props.stateMachine, { credentialsRole: apiRole, corsEnabled: corsEnabled, - includeRequestContext: props.includeRequestContext, + requestContext: props.requestContext, + action: 'StartSyncExecution', }), ...props, }); @@ -74,7 +96,8 @@ export class StepFunctionsRestApi extends RestApi { if (!corsEnabled) { this.root.addMethod('ANY', new StepFunctionsIntegration(props.stateMachine, { credentialsRole: apiRole, - includeRequestContext: props.includeRequestContext, + requestContext: props.requestContext, + action: 'StartSyncExecution', }), { methodResponses: [ ...methodResp, diff --git a/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts b/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts index 4cf14b384ce05..aceb3a2d4b795 100644 --- a/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts @@ -170,7 +170,26 @@ describe('Step Functions api', () => { const api = new apigw.StepFunctionsRestApi(stack, 'StepFunctionsRestApi', { stateMachine: stateMachine, - includeRequestContext: true, + requestContext: { + accountId: true, + apiId: true, + apiKey: true, + authorizerPrincipalId: true, + caller: true, + cognitoAuthenticationProvider: true, + cognitoAuthenticationType: true, + cognitoIdentityId: true, + cognitoIdentityPoolId: true, + httpMethod: true, + stage: true, + sourceIp: true, + user: true, + userAgent: true, + userArn: true, + requestId: true, + resourceId: true, + resourcePath: true, + }, }); expect(() => { From b4c5b532d07a7b9677f079928ee2a18b03e3ff69 Mon Sep 17 00:00:00 2001 From: Saqib Dhuka Date: Sat, 20 Nov 2021 00:18:06 +0000 Subject: [PATCH 06/35] Added more integration tests. Created a VTL template. Added more parameters to the construct to enable users to modify the request with path, header, querystring and requestContext. Updated the README. --- packages/@aws-cdk/aws-apigateway/README.md | 61 +++- .../aws-apigateway/lib/integrations/index.ts | 2 +- .../integrations/request-context-builder.ts | 163 ----------- .../lib/integrations/request-context.ts | 112 ++++++++ .../lib/integrations/stepfunctions.ts | 195 +++++++++---- .../lib/integrations/stepfunctions.vtl | 57 ++++ .../aws-apigateway/lib/stepfunctions-api.ts | 75 +++-- ...stepfunctions-api-all-params.expected.json | 261 ++++++++++++++++++ .../integ.stepfunctions-api-all-params.ts | 64 +++++ ...ns-api-request-context-empty.expected.json | 261 ++++++++++++++++++ ...stepfunctions-api-request-context-empty.ts | 42 +++ ...-context-multiple-properties.expected.json | 261 ++++++++++++++++++ ...api-request-context-multiple-properties.ts | 45 +++ ...uest-context-single-property.expected.json | 261 ++++++++++++++++++ ...ons-api-request-context-single-property.ts | 44 +++ .../integ.stepfunctions-api.expected.json | 14 +- .../test/integrations/stepfunctions.test.ts | 8 +- .../test/stepfunctions-api.test.ts | 84 +++++- .../aws-stepfunctions/lib/state-machine.ts | 20 ++ .../test/state-machine-resources.test.ts | 34 +++ 20 files changed, 1782 insertions(+), 282 deletions(-) delete mode 100644 packages/@aws-cdk/aws-apigateway/lib/integrations/request-context-builder.ts create mode 100644 packages/@aws-cdk/aws-apigateway/lib/integrations/request-context.ts create mode 100644 packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.vtl create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-all-params.expected.json create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-all-params.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-empty.expected.json create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-empty.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-multiple-properties.expected.json create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-multiple-properties.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-single-property.expected.json create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-single-property.ts diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 8c3f9ea1b7a5b..ab69920aa53d4 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -111,13 +111,17 @@ item.addMethod('DELETE', new apigateway.HttpIntegration('http://amazon.com')); You can use Amazon API Gateway with AWS Step Functions as the backend integration, specifically Synchronous Express Workflows. -The `StepFunctionsRestApi` construct makes this easy and also sets up input, output and error mapping. The `StepFunctionsRestApi` construct sets up the API Gateway REST API with an `ANY` HTTP method and sets up the api role with the required permission to invoke `StartSyncExecution` action on the AWS StepFunctions state machine. This will enable you to invoke any one of the API Gateway HTTP methods and get a response from the backend AWS StepFunctions state machine. Invoking any of the HTTP Method, using the example below, will return a "Hello!" message as the Response Body. +The `StepFunctionsRestApi` only supports integration with Synchronous Express state machine. The `StepFunctionsRestApi` construct makes this easy and also sets up input, output and error mapping. The `StepFunctionsRestApi` construct sets up the API Gateway REST API with an `ANY` HTTP method and sets up the api role with the required permission to invoke `StartSyncExecution` action on the AWS StepFunctions state machine. It sets up up a `prod` stage by default. This will enable you to invoke any of the API Gateway HTTP methods for that `prod` and get a response from the backend AWS Step Functions execution. Invoking either GET or POST in the example below will send the request to the state machine as a new execution. On success, an HTTP code '200' is returned with ONLY the execution output as the Response Body. If the state machine execution fails, an HTTP '500' error response is returned with the error and cause of the execution as the Response Body. If the request is invalid (ex. bad execution input) an HTTP '400' error is returned. + +As part of the API Gateway integration with Step Functions, it is possible to opt-in to include requestContext, headers, path, and querystring to the execution input. By default, these paramaters are not included in order to reduce payload size sent to Step Functions. + +More details about AWS Step Functions payload limit can be found at https://docs.aws.amazon.com/step-functions/latest/dg/limits-overview.html#service-limits-task-executions. The following code defines a REST API that routes all requests to the specified AWS StepFunctions state machine: ```ts const machineDefinition = new sfn.Pass(this, 'PassState', { - result: {value:"Hello!"}, + }) const stateMachine: sfn.IStateMachine = new sfn.StateMachine(this, 'StateMachine', { @@ -130,6 +134,59 @@ new apigateway.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { }); ``` +Here are a few examples: + +Example 1: POST with default configuration + +```json +POST / +{ + "customerId": 1 +} +``` + +Step Functions will wrap the request body inside a `body` key as it is possible to have other keys within the request (ex. requestContext, header, path, and/or querystring): + +```json +{ + "body": { + "customerId": 1 + } +} +``` + +Example2: GET with API-Gateway path enabled. Set up API Gateway with resources `/user/{userId}`. Invoking 'https://\.execute-api.\.amazonaws.com/prod/users/1'. + +Request: + +```json +{ + "body": {}, + "path": "/users/{userid}", +} +``` + +Example 3: GET with API-Gateway requestContext, header, and querystring enabled. Invoking 'https://\.execute-api.\.amazonaws.com/prod?customerId=1'. + +Request: + +```json +{ + "body": {}, + "requestContext": { + "accountId": "...", + "apiKey": "...", + }, + "header": { + "Accept": "...", + "CloudFront-Forwarded-Proto": "...", + }, + "querystring": { + "customerId": 1 + } +} +``` + ### Breaking up Methods and Resources across Stacks It is fairly common for REST APIs with a large number of Resources and Methods to hit the [CloudFormation diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/index.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/index.ts index f1fe3526fc324..9ebc36bf92a58 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/index.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/index.ts @@ -3,4 +3,4 @@ export * from './lambda'; export * from './http'; export * from './mock'; export * from './stepfunctions'; -export * from './request-context-builder'; +export * from './request-context'; diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/request-context-builder.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/request-context-builder.ts deleted file mode 100644 index 0c3def62743ba..0000000000000 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/request-context-builder.ts +++ /dev/null @@ -1,163 +0,0 @@ -/** - * Request Context interface - */ -export interface RequestContext { - /** - * Account ID string - * @default false - */ - readonly accountId?: boolean; - - /** - * Api ID string - * @default false - */ - readonly apiId?: boolean; - - /** - * Api Key string - * @default false - */ - readonly apiKey?: boolean; - - /** - * Authorizer Principal ID string - * @default false - */ - readonly authorizerPrincipalId?: boolean; - - /** - * Caller string - * @default false - */ - readonly caller?: boolean; - - /** - * Cognito Authentication Provider string - * @default false - */ - readonly cognitoAuthenticationProvider?: boolean; - - /** - * Cognito Authentication Type string - * @default false - */ - readonly cognitoAuthenticationType?: boolean; - - /** - * Cognito Identity ID string - * @default false - */ - readonly cognitoIdentityId?: boolean; - - /** - * Cognito Identity Pool ID string - * @default false - */ - readonly cognitoIdentityPoolId?: boolean; - - /** - * Http Method string - * @default false - */ - readonly httpMethod?: boolean; - - /** - * Stage string - * @default false - */ - readonly stage?: boolean; - - /** - * Source IP string - * @default false - */ - readonly sourceIp?: boolean; - - /** - * User string - * @default false - */ - readonly user?: boolean; - - /** - * User Agent string - * @default false - */ - readonly userAgent?: boolean; - - /** - * User Arn string - * @default false - */ - readonly userArn?: boolean; - - /** - * Request ID string - * @default false - */ - readonly requestId?: boolean; - - /** - * Resource ID string - * @default false - */ - readonly resourceId?: boolean; - - /** - * Resource Path string - * @default false - */ - readonly resourcePath?: boolean; -} - -/** - * Builder to build Request Context object - */ -export class RequestContextBuilder { - - constructor() {} - - /** - * Returns the requestContext String - * @param requestContextInterface - * @returns requestContextString - */ - public with(requestContextInterface:RequestContext): string { - const bodyStr: string = '"body": $util.escapeJavaScript($input.json(\'$\')),'; - const contextStr: string = '"requestContext": {'; - const accountIdStr: string = (requestContextInterface.accountId) ? '"accountId":"$context.identity.accountId",' : ''; - const apiIdStr: string = (requestContextInterface.apiId) ? '"apiId":"$context.apiId",' : ''; - const apiKeyStr: string = (requestContextInterface.apiKey) ? '"apiKey":"$context.identity.apiKey",' : ''; - const authorizerPrincipalIdStr: string = (requestContextInterface.authorizerPrincipalId) ? '"authorizerPrincipalId":"$context.authorizer.principalId",' : ''; - const callerStr: string = (requestContextInterface.caller) ? '"caller":"$context.identity.caller",' : ''; - const cognitoAuthenticationProviderStr: string = (requestContextInterface.cognitoAuthenticationProvider) ? '"cognitoAuthenticationProvider":"$context.identity.cognitoAuthenticationProvider",' : ''; - const cognitoAuthenticationTypeStr: string = (requestContextInterface.cognitoAuthenticationType) ? '"cognitoAuthenticationType":"$context.identity.cognitoAuthenticationType",' : ''; - const cognitoIdentityIdStr: string = (requestContextInterface.cognitoIdentityId) ? '"cognitoIdentityId":"$context.identity.cognitoIdentityId",' : ''; - const cognitoIdentityPoolIdStr: string = (requestContextInterface.cognitoIdentityPoolId) ? '"cognitoIdentityPoolId":"$context.identity.cognitoIdentityPoolId",' : ''; - const httpMethodStr: string = (requestContextInterface.httpMethod) ? '"httpMethod":"$context.httpMethod",' : ''; - const stageStr: string = (requestContextInterface.stage) ? '"stage":"$context.stage",' : ''; - const sourceIpStr: string = (requestContextInterface.sourceIp) ? '"sourceIp":"$context.identity.sourceIp",' : ''; - const userStr: string = (requestContextInterface.user) ? '"user":"$context.identity.user",' : ''; - const userAgentStr: string = (requestContextInterface.userAgent) ? '"userAgent":"$context.identity.userAgent",' : ''; - const userArnStr: string = (requestContextInterface.userArn) ? '"userArn":"$context.identity.userArn",' : ''; - const requestIdStr: string = (requestContextInterface.requestId) ? '"requestId":"$context.requestId",' : ''; - const resourceIdStr: string = (requestContextInterface.resourceId) ? '"resourceId":"$context.resourceId",' : ''; - const resourcePathStr: string = (requestContextInterface.resourcePath) ? '"resourcePath":"$context.resourcePath",' : ''; - const endStr = '}'; - - let requestContextString :string = bodyStr + contextStr + accountIdStr + apiIdStr + apiKeyStr + - authorizerPrincipalIdStr + callerStr + cognitoAuthenticationProviderStr + cognitoAuthenticationTypeStr + - cognitoIdentityIdStr + cognitoIdentityPoolIdStr + httpMethodStr + stageStr + sourceIpStr + userStr + - userAgentStr + userArnStr + requestIdStr + resourceIdStr + resourcePathStr + endStr; - - if (requestContextString !== (bodyStr + contextStr + endStr)) { - //Removing the last comma froom the string only if it has a value inside - requestContextString = requestContextString.substring(0, requestContextString.length-2) + '}'; - } - - return requestContextString; - } - - -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/request-context.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/request-context.ts new file mode 100644 index 0000000000000..f9f80ff7563b3 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/request-context.ts @@ -0,0 +1,112 @@ +/** + * Request Context interface + */ +export interface RequestContext { + /** + * Account ID string + * @default false + */ + readonly accountId?: boolean; + + /** + * Api ID string + * @default false + */ + readonly apiId?: boolean; + + /** + * Api Key string + * @default false + */ + readonly apiKey?: boolean; + + /** + * Authorizer Principal ID string + * @default false + */ + readonly authorizerPrincipalId?: boolean; + + /** + * Caller string + * @default false + */ + readonly caller?: boolean; + + /** + * Cognito Authentication Provider string + * @default false + */ + readonly cognitoAuthenticationProvider?: boolean; + + /** + * Cognito Authentication Type string + * @default false + */ + readonly cognitoAuthenticationType?: boolean; + + /** + * Cognito Identity ID string + * @default false + */ + readonly cognitoIdentityId?: boolean; + + /** + * Cognito Identity Pool ID string + * @default false + */ + readonly cognitoIdentityPoolId?: boolean; + + /** + * Http Method string + * @default false + */ + readonly httpMethod?: boolean; + + /** + * Stage string + * @default false + */ + readonly stage?: boolean; + + /** + * Source IP string + * @default false + */ + readonly sourceIp?: boolean; + + /** + * User string + * @default false + */ + readonly user?: boolean; + + /** + * User Agent string + * @default false + */ + readonly userAgent?: boolean; + + /** + * User Arn string + * @default false + */ + readonly userArn?: boolean; + + /** + * Request ID string + * @default false + */ + readonly requestId?: boolean; + + /** + * Resource ID string + * @default false + */ + readonly resourceId?: boolean; + + /** + * Resource Path string + * @default false + */ + readonly resourcePath?: boolean; +} diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts index 0a0ed82ac1c91..1040c00a95ec5 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts @@ -1,11 +1,12 @@ +import * as fs from 'fs'; +import * as path from 'path'; import * as iam from '@aws-cdk/aws-iam'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import { Token } from '@aws-cdk/core'; -import { RequestContextBuilder, RequestContext } from '.'; +import { RequestContext } from '.'; import { IntegrationConfig, IntegrationOptions, PassthroughBehavior } from '../integration'; import { Method } from '../method'; import { AwsIntegration } from './aws'; - /** * Options when configuring Step Functions integration with Rest API */ @@ -24,45 +25,30 @@ export interface StepFunctionsIntegrationOptions extends IntegrationOptions { readonly corsEnabled?: boolean; /** - * You can add requestContext (similar to input requestContext from lambda input) - * to the input. The 'requestContext' parameter includes account ID, user identity, etc. - * that can be used by customers that want to know the identity of authorized users on - * the state machine side. You can individually select the keys you want by setting them to true. - * The following code defines a REST API like above but also adds 'requestContext' to the input - * of the State Machine: - * - * @example - * - * const stateMachine = new stepFunctions.StateMachine(this, 'StateMachine', ...); - * new apigateway.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { - * stateMachine: stateMachine, - * requestContext: { - * accountId: true, - * apiId: true, - * apiKey: true, - * authorizerPrincipalId: true, - * caller: true, - * cognitoAuthenticationProvider: true, - * cognitoAuthenticationType: true, - * cognitoIdentityId: true, - * cognitoIdentityPoolId: true, - * httpMethod: true, - * stage: true, - * sourceIp: true, - * user: true, - * userAgent: true, - * userArn: true, - * requestId: true, - * resourceId: true, - * resourcePath: true, - * }, - * }); + * Which details of the incoming request must be passed onto the underlying state machine, + * such as, account id, user identity, request id, etc. * * @default - all parameters within request context will be set as false */ readonly requestContext?: RequestContext; + /** + * Check if querystring is to be included inside the execution input + * @default false + */ + readonly queryString?: boolean; + /** + * Check if path is to be included inside the execution input + * @default false + */ + readonly path?: boolean; + + /** + * Check if header is to be included inside the execution input + * @default false + */ + readonly headers?: boolean; } /** * Integrates a Synchronous Express State Machine from AWS Step Functions to an API Gateway method. @@ -76,10 +62,9 @@ export class StepFunctionsIntegration extends AwsIntegration { private readonly stateMachine: sfn.IStateMachine; private readonly action: string; constructor(stateMachine: sfn.IStateMachine, options: StepFunctionsIntegrationOptions = { action: 'StartSyncExecution' }) { - let requestContextRequested: boolean = (options.requestContext) ? true: false; const integResponse = integrationResponse(); - const requestTemplate = requestTemplates(stateMachine, requestContextRequested, options.requestContext); + const requestTemplate = requestTemplates(stateMachine, options); super({ service: 'states', action: options.action, @@ -88,6 +73,7 @@ export class StepFunctionsIntegration extends AwsIntegration { integrationResponses: integResponse, passthroughBehavior: PassthroughBehavior.NEVER, requestTemplates: requestTemplate, + ...options, }, }); @@ -125,6 +111,12 @@ export class StepFunctionsIntegration extends AwsIntegration { } } +/** + * Defines the integration response that passes the result, on success, + * or the error, on failure, from the backend the client. + * + * @returns integrationResponse + */ function integrationResponse() { const errorResponse = [ { @@ -175,8 +167,41 @@ function integrationResponse() { return integResponse; } -function requestTemplates(stateMachine: sfn.IStateMachine, includeRequestContext: boolean, requestContextObj: RequestContext | undefined) { - const templateStr = templateString(stateMachine, includeRequestContext, requestContextObj); +/** + * Checks each property of the RequestContext Object to see if request context has + * a property specified and then return true or false. + * @param requestContextObj + * @returns boolean + */ +function checkIncludeRequestContext(requestContextObj: RequestContext | undefined) { + + if (requestContextObj) { + if (!requestContextObj.accountId && !requestContextObj.apiId && !requestContextObj.apiKey && + !requestContextObj.authorizerPrincipalId && !requestContextObj.caller && + !requestContextObj.cognitoAuthenticationProvider && !requestContextObj.cognitoAuthenticationType && + !requestContextObj.cognitoIdentityId && !requestContextObj.cognitoIdentityPoolId && + !requestContextObj.httpMethod && !requestContextObj.stage && !requestContextObj.sourceIp && + !requestContextObj.user && !requestContextObj.userAgent && + !requestContextObj.userArn && !requestContextObj.requestId && + !requestContextObj.resourceId && !requestContextObj.resourcePath) { + return false; + } else { + return true; + } + } else { + return false; + } +} + +/** + * Defines the request template that will be used for the integration + * @param stateMachine + * @param options + * @returns requestTemplate + */ +function requestTemplates(stateMachine: sfn.IStateMachine, options: StepFunctionsIntegrationOptions) { + let includeRequestContext: boolean = checkIncludeRequestContext(options.requestContext); + const templateStr = templateString(stateMachine, includeRequestContext, options); const requestTemplate: { [contentType:string] : string } = { @@ -186,33 +211,82 @@ function requestTemplates(stateMachine: sfn.IStateMachine, includeRequestContext return requestTemplate; } -function templateString(stateMachine: sfn.IStateMachine, includeRequestContext: boolean, requestContextObj: RequestContext | undefined): string { +/** + * Reads the VTL template and returns the template string to be used + * for the request template. + * + * @param stateMachine + * @param includeRequestContext + * @param options + * @reutrns templateString + */ +function templateString(_stateMachine: sfn.IStateMachine, _includeRequestContext: boolean, _options: StepFunctionsIntegrationOptions): string { let templateStr: string; - if (includeRequestContext) { - const requestContextStr = requestContext(requestContextObj); + let requestContextStr = ''; - const search = '"'; - const replaceWith = '\\"'; - const requestContextStrModified = requestContextStr.split(search).join(replaceWith); - templateStr = ` - #set($allParams = $input.params()) - { - "input": "{${requestContextStrModified}}", - "stateMachineArn": "${stateMachine.stateMachineArn}" - }`; - } else { - templateStr = ` - #set($inputRoot = $input.path('$')) { - "input": "$util.escapeJavaScript($input.json('$'))", - "stateMachineArn": "${stateMachine.stateMachineArn}" - }`; + const includeHeader: string = (_options.headers) ? 'true': 'false'; + const includeQueryString: string = (_options.queryString) ? 'true': 'false'; + const includePath: string = (_options.path) ? 'true': 'false'; + + if (_includeRequestContext) { + requestContextStr = requestContext(_options.requestContext); } + + templateStr = fs.readFileSync(path.join(__dirname, 'stepfunctions.vtl'), { encoding: 'utf-8' }); + templateStr = templateStr.replace('%STATEMACHINE%', _stateMachine.stateMachineArn); + templateStr = templateStr.replace('%INCLUDE_HEADERS%', includeHeader); + templateStr = templateStr.replace('%INCLUDE_QUERYSTRING%', includeQueryString); + templateStr = templateStr.replace('%INCLUDE_PATH%', includePath); + templateStr = templateStr.replace('%REQUESTCONTEXT%', requestContextStr); + + return templateStr; } +/** + * Builder function that builds the request context string + * for when request context is asked for the execution input. + * + * @param requestContextInterface + * @returns reqeustContextStr + */ +function requestContextBuilder(requestContextInterface:RequestContext): string { + const contextStr: string = '"requestContext": {'; + const accountIdStr: string = (requestContextInterface.accountId) ? '"accountId":"$context.identity.accountId",' : ''; + const apiIdStr: string = (requestContextInterface.apiId) ? '"apiId":"$context.apiId",' : ''; + const apiKeyStr: string = (requestContextInterface.apiKey) ? '"apiKey":"$context.identity.apiKey",' : ''; + const authorizerPrincipalIdStr: string = (requestContextInterface.authorizerPrincipalId) ? '"authorizerPrincipalId":"$context.authorizer.principalId",' : ''; + const callerStr: string = (requestContextInterface.caller) ? '"caller":"$context.identity.caller",' : ''; + const cognitoAuthenticationProviderStr: string = (requestContextInterface.cognitoAuthenticationProvider) ? '"cognitoAuthenticationProvider":"$context.identity.cognitoAuthenticationProvider",' : ''; + const cognitoAuthenticationTypeStr: string = (requestContextInterface.cognitoAuthenticationType) ? '"cognitoAuthenticationType":"$context.identity.cognitoAuthenticationType",' : ''; + const cognitoIdentityIdStr: string = (requestContextInterface.cognitoIdentityId) ? '"cognitoIdentityId":"$context.identity.cognitoIdentityId",' : ''; + const cognitoIdentityPoolIdStr: string = (requestContextInterface.cognitoIdentityPoolId) ? '"cognitoIdentityPoolId":"$context.identity.cognitoIdentityPoolId",' : ''; + const httpMethodStr: string = (requestContextInterface.httpMethod) ? '"httpMethod":"$context.httpMethod",' : ''; + const stageStr: string = (requestContextInterface.stage) ? '"stage":"$context.stage",' : ''; + const sourceIpStr: string = (requestContextInterface.sourceIp) ? '"sourceIp":"$context.identity.sourceIp",' : ''; + const userStr: string = (requestContextInterface.user) ? '"user":"$context.identity.user",' : ''; + const userAgentStr: string = (requestContextInterface.userAgent) ? '"userAgent":"$context.identity.userAgent",' : ''; + const userArnStr: string = (requestContextInterface.userArn) ? '"userArn":"$context.identity.userArn",' : ''; + const requestIdStr: string = (requestContextInterface.requestId) ? '"requestId":"$context.requestId",' : ''; + const resourceIdStr: string = (requestContextInterface.resourceId) ? '"resourceId":"$context.resourceId",' : ''; + const resourcePathStr: string = (requestContextInterface.resourcePath) ? '"resourcePath":"$context.resourcePath",' : ''; + const endStr = '}'; + + let requestContextString :string = contextStr + accountIdStr + apiIdStr + apiKeyStr + + authorizerPrincipalIdStr + callerStr + cognitoAuthenticationProviderStr + cognitoAuthenticationTypeStr + + cognitoIdentityIdStr + cognitoIdentityPoolIdStr + httpMethodStr + stageStr + sourceIpStr + userStr + + userAgentStr + userArnStr + requestIdStr + resourceIdStr + resourcePathStr + endStr; + + if (requestContextString !== (contextStr + endStr)) { + //Removing the last comma froom the string only if it has a value inside + requestContextString = requestContextString.substring(0, requestContextString.length-2) + '}'; + } + return requestContextString; +} + function requestContext(requestContextObj: RequestContext | undefined): string { - const requestContextStr: string = new RequestContextBuilder().with({ + const requestContextStr: string = requestContextBuilder({ accountId: (requestContextObj) ? requestContextObj?.accountId : false, apiId: (requestContextObj) ? requestContextObj?.apiId : false, apiKey: (requestContextObj) ? requestContextObj?.apiKey : false, @@ -233,5 +307,8 @@ function requestContext(requestContextObj: RequestContext | undefined): string { resourcePath: (requestContextObj) ? requestContextObj?.resourcePath : false, }); - return requestContextStr; + const search = '"'; + const replaceWith = '@@'; + const requestContextStrModified = requestContextStr.split(search).join(replaceWith); + return requestContextStrModified; } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.vtl b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.vtl new file mode 100644 index 0000000000000..e59c61aa93fff --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.vtl @@ -0,0 +1,57 @@ +## Velocity Template used for API Gateway request mapping template +## +## This template forwards the request body, header, path, and querystring +## to the execution input of the state machine. +## +## "@@" is used here as a placeholder for '"' to avoid using escape characters. + +#set($inputString = '') +#set($includeHeaders = %INCLUDE_HEADERS%) +#set($includeQueryString = %INCLUDE_QUERYSTRING%) +#set($includePath = %INCLUDE_PATH%) +#set($allParams = $input.params()) +{ + "stateMachineArn": "%STATEMACHINE%", + + #set($inputString = "$inputString,@@body@@: $input.body") + + #if ($includeHeaders) + #set($inputString = "$inputString, @@header@@:{") + #foreach($paramName in $allParams.header.keySet()) + #set($inputString = "$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@") + #if($foreach.hasNext) + #set($inputString = "$inputString,") + #end + #end + #set($inputString = "$inputString }") + + #end + + #if ($includeQueryString) + + #set($inputString = "$inputString, @@querystring@@:{") + #foreach($paramName in $allParams.querystring.keySet()) + #set($inputString = "$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@") + #if($foreach.hasNext) + #set($inputString = "$inputString,") + #end + #end + #set($inputString = "$inputString }") + #end + + #if ($includePath) + #set($inputString = "$inputString, @@path@@: @@$context.resourcePath@@") + #end + + #set($requestContext = "%REQUESTCONTEXT%") + ## Check if the request context should be included as part of the execution input + #if($requestContext && !$requestContext.empty) + #set($inputString = "$inputString,") + #set($inputString = "$inputString $requestContext") + #end + + #set($inputString = "$inputString}") + #set($inputString = $inputString.replaceAll("@@",'"')) + #set($len = $inputString.length() - 1) + "input": "{$util.escapeJavaScript($inputString.substring(1,$len))}" +} diff --git a/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts b/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts index 20837dd95ee08..2479b8c47d939 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts @@ -27,36 +27,27 @@ export interface StepFunctionsRestApiProps extends RestApiProps { * The following code defines a REST API like above but also adds 'requestContext' to the input * of the State Machine: * - * @example - * - * const stateMachine = new stepFunctions.StateMachine(this, 'StateMachine', ...); - * new apigateway.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { - * stateMachine: stateMachine, - * requestContext: { - * accountId: true, - * apiId: true, - * apiKey: true, - * authorizerPrincipalId: true, - * caller: true, - * cognitoAuthenticationProvider: true, - * cognitoAuthenticationType: true, - * cognitoIdentityId: true, - * cognitoIdentityPoolId: true, - * httpMethod: true, - * stage: true, - * sourceIp: true, - * user: true, - * userAgent: true, - * userArn: true, - * requestId: true, - * resourceId: true, - * resourcePath: true, - * }, - * }); - * * @default - all parameters within request context will be set as false */ readonly requestContext?: RequestContext; + + /** + * Check if querystring is to be included inside the execution input + * @default false + */ + readonly queryString?: boolean; + + /** + * Check if path is to be included inside the execution input + * @default false + */ + readonly path?: boolean; + + /** + * Check if header is to be included inside the execution input + * @default false + */ + readonly headers?: boolean; } /** @@ -98,6 +89,9 @@ export class StepFunctionsRestApi extends RestApi { credentialsRole: apiRole, requestContext: props.requestContext, action: 'StartSyncExecution', + queryString: props.queryString, + path: props.path, + headers: props.headers, }), { methodResponses: [ ...methodResp, @@ -106,28 +100,29 @@ export class StepFunctionsRestApi extends RestApi { } } } - +/** + * Defines the IAM Role for API Gateway with required permisisons + * to perform action on the state machine. + * + * @param scope + * @param props + * @returns Role - IAM Role + */ function role(scope: Construct, props: StepFunctionsRestApiProps): iam.Role { - const apiName: string = props.stateMachine + '-apiRole'; + const apiName: string = props.stateMachine + '-apiRoleNew'; 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.stateMachine.stateMachineArn], - }), - ], - }), - ); + props.stateMachine.grantStartSyncExecution(apiRole); return apiRole; } +/** + * Defines the method response modelfor each HTTP code response + * @returns methodResponse + */ function methodResponse() { const methodResp = [ { diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-all-params.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-all-params.expected.json new file mode 100644 index 0000000000000..859b070be3ea5 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-all-params.expected.json @@ -0,0 +1,261 @@ +{ + "Resources": { + "StateMachineRoleB840431D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, + "DefinitionString": "{\"StartAt\":\"PassTask\",\"States\":{\"PassTask\":{\"Type\":\"Pass\",\"Result\":\"Hello\",\"End\":true}}}", + "StateMachineType": "EXPRESS" + }, + "DependsOn": [ + "StateMachineRoleB840431D" + ] + }, + "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewDefaultPolicy6DEF984E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartSyncExecution", + "Effect": "Allow", + "Resource": { + "Ref": "StateMachine2E01A3A5" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewDefaultPolicy6DEF984E", + "Roles": [ + { + "Ref": "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0" + } + ] + } + }, + "StepFunctionsRestApiC6E3E883": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "StepFunctionsRestApi" + } + }, + "StepFunctionsRestApiCloudWatchRoleB06ACDB9": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + } + }, + "StepFunctionsRestApiAccountBD0CCC0E": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "StepFunctionsRestApiCloudWatchRoleB06ACDB9", + "Arn" + ] + } + }, + "DependsOn": [ + "StepFunctionsRestApiC6E3E883" + ] + }, + "StepFunctionsRestApiANY7699CA92": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Fn::GetAtt": [ + "StepFunctionsRestApiC6E3E883", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "StepFunctionsRestApiC6E3E883" + }, + "AuthorizationType": "NONE", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0", + "Arn" + ] + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": [ + { + "ResponseTemplates": { + "application/json": "#set($inputRoot = $input.path('$'))\n #if($input.path('$.status').toString().equals(\"FAILED\"))\n #set($context.responseOverride.status = 500)\n { \n \"error\": \"$input.path('$.error')\",\n \"cause\": \"$input.path('$.cause')\"\n }\n #else\n $input.path('$.output')\n #end" + }, + "StatusCode": "200" + }, + { + "ResponseTemplates": { + "application/json": "{\n \"error\": \"Bad input!\"\n }" + }, + "SelectionPattern": "4\\d{2}", + "StatusCode": "400" + }, + { + "ResponseTemplates": { + "application/json": "\"error\": $input.path('$.error')" + }, + "SelectionPattern": "5\\d{2}", + "StatusCode": "500" + } + ], + "PassthroughBehavior": "NEVER", + "RequestTemplates": { + "application/json": { + "Fn::Join": [ + "", + [ + "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = true)\n#set($includeQueryString = true)\n#set($includePath = true)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", + { + "Ref": "StateMachine2E01A3A5" + }, + "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n \n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@: @@$context.resourcePath@@\")\n #end\n \n #set($requestContext = \"@@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@@}\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n" + ] + ] + } + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":states:action/StartSyncExecution" + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseModels": { + "application/json": "Empty" + }, + "StatusCode": "200" + }, + { + "ResponseModels": { + "application/json": "Error" + }, + "StatusCode": "400" + }, + { + "ResponseModels": { + "application/json": "Error" + }, + "StatusCode": "500" + } + ] + } + }, + "deployment33381975b5dafda9a97138f301ea25da405640e8": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "StepFunctionsRestApiC6E3E883" + } + }, + "DependsOn": [ + "StepFunctionsRestApiANY7699CA92" + ] + }, + "stage0661E4AC": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "StepFunctionsRestApiC6E3E883" + }, + "DeploymentId": { + "Ref": "deployment33381975b5dafda9a97138f301ea25da405640e8" + }, + "StageName": "prod" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-all-params.ts b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-all-params.ts new file mode 100644 index 0000000000000..2e7d99b0eb15b --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-all-params.ts @@ -0,0 +1,64 @@ +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as apigw from '../lib'; + +/** + * Stack verification steps: + * * `curl -X POST 'https://.execute-api..amazonaws.com/prod' \ + * * -d '{"key":"Hello"}' -H 'Content-Type: application/json'` + * The above should return a "Hello" response + */ + +class StepFunctionsRestApiDeploymentStack extends cdk.Stack { + constructor(scope: Construct) { + super(scope, 'StepFunctionsRestApiDeploymentStack'); + + const passTask = new sfn.Pass(this, 'PassTask', { + result: { value: 'Hello' }, + }); + + const stateMachine = new sfn.StateMachine(this, 'StateMachine', { + definition: passTask, + stateMachineType: sfn.StateMachineType.EXPRESS, + }); + + const api = new apigw.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { + deploy: false, + stateMachine: stateMachine, + headers: true, + queryString: true, + path: true, + requestContext: { + accountId: true, + apiId: true, + apiKey: true, + authorizerPrincipalId: true, + caller: true, + cognitoAuthenticationProvider: true, + cognitoAuthenticationType: true, + cognitoIdentityId: true, + cognitoIdentityPoolId: true, + httpMethod: true, + stage: true, + sourceIp: true, + user: true, + userAgent: true, + userArn: true, + requestId: true, + resourceId: true, + resourcePath: true, + }, + }); + + api.deploymentStage = new apigw.Stage(this, 'stage', { + deployment: new apigw.Deployment(this, 'deployment', { + api, + }), + }); + } +} + +const app = new cdk.App(); +new StepFunctionsRestApiDeploymentStack(app); +app.synth(); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-empty.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-empty.expected.json new file mode 100644 index 0000000000000..091dbb0e89817 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-empty.expected.json @@ -0,0 +1,261 @@ +{ + "Resources": { + "StateMachineRoleB840431D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, + "DefinitionString": "{\"StartAt\":\"PassTask\",\"States\":{\"PassTask\":{\"Type\":\"Pass\",\"Result\":\"Hello\",\"End\":true}}}", + "StateMachineType": "EXPRESS" + }, + "DependsOn": [ + "StateMachineRoleB840431D" + ] + }, + "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewDefaultPolicy6DEF984E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartSyncExecution", + "Effect": "Allow", + "Resource": { + "Ref": "StateMachine2E01A3A5" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewDefaultPolicy6DEF984E", + "Roles": [ + { + "Ref": "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0" + } + ] + } + }, + "StepFunctionsRestApiC6E3E883": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "StepFunctionsRestApi" + } + }, + "StepFunctionsRestApiCloudWatchRoleB06ACDB9": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + } + }, + "StepFunctionsRestApiAccountBD0CCC0E": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "StepFunctionsRestApiCloudWatchRoleB06ACDB9", + "Arn" + ] + } + }, + "DependsOn": [ + "StepFunctionsRestApiC6E3E883" + ] + }, + "StepFunctionsRestApiANY7699CA92": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Fn::GetAtt": [ + "StepFunctionsRestApiC6E3E883", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "StepFunctionsRestApiC6E3E883" + }, + "AuthorizationType": "NONE", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0", + "Arn" + ] + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": [ + { + "ResponseTemplates": { + "application/json": "#set($inputRoot = $input.path('$'))\n #if($input.path('$.status').toString().equals(\"FAILED\"))\n #set($context.responseOverride.status = 500)\n { \n \"error\": \"$input.path('$.error')\",\n \"cause\": \"$input.path('$.cause')\"\n }\n #else\n $input.path('$.output')\n #end" + }, + "StatusCode": "200" + }, + { + "ResponseTemplates": { + "application/json": "{\n \"error\": \"Bad input!\"\n }" + }, + "SelectionPattern": "4\\d{2}", + "StatusCode": "400" + }, + { + "ResponseTemplates": { + "application/json": "\"error\": $input.path('$.error')" + }, + "SelectionPattern": "5\\d{2}", + "StatusCode": "500" + } + ], + "PassthroughBehavior": "NEVER", + "RequestTemplates": { + "application/json": { + "Fn::Join": [ + "", + [ + "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = false)\n#set($includePath = false)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", + { + "Ref": "StateMachine2E01A3A5" + }, + "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n \n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@: @@$context.resourcePath@@\")\n #end\n \n #set($requestContext = \"\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n" + ] + ] + } + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":states:action/StartSyncExecution" + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseModels": { + "application/json": "Empty" + }, + "StatusCode": "200" + }, + { + "ResponseModels": { + "application/json": "Error" + }, + "StatusCode": "400" + }, + { + "ResponseModels": { + "application/json": "Error" + }, + "StatusCode": "500" + } + ] + } + }, + "deployment33381975b5dafda9a97138f301ea25da405640e8": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "StepFunctionsRestApiC6E3E883" + } + }, + "DependsOn": [ + "StepFunctionsRestApiANY7699CA92" + ] + }, + "stage0661E4AC": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "StepFunctionsRestApiC6E3E883" + }, + "DeploymentId": { + "Ref": "deployment33381975b5dafda9a97138f301ea25da405640e8" + }, + "StageName": "prod" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-empty.ts b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-empty.ts new file mode 100644 index 0000000000000..53ba702eee948 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-empty.ts @@ -0,0 +1,42 @@ +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as apigw from '../lib'; + +/** + * Stack verification steps: + * * `curl -X POST 'https://.execute-api..amazonaws.com/prod' \ + * * -d '{"key":"Hello"}' -H 'Content-Type: application/json'` + * The above should return a "Hello" response + */ + +class StepFunctionsRestApiDeploymentStack extends cdk.Stack { + constructor(scope: Construct) { + super(scope, 'StepFunctionsRestApiDeploymentStack'); + + const passTask = new sfn.Pass(this, 'PassTask', { + result: { value: 'Hello' }, + }); + + const stateMachine = new sfn.StateMachine(this, 'StateMachine', { + definition: passTask, + stateMachineType: sfn.StateMachineType.EXPRESS, + }); + + const api = new apigw.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { + deploy: false, + stateMachine: stateMachine, + requestContext: {}, + }); + + api.deploymentStage = new apigw.Stage(this, 'stage', { + deployment: new apigw.Deployment(this, 'deployment', { + api, + }), + }); + } +} + +const app = new cdk.App(); +new StepFunctionsRestApiDeploymentStack(app); +app.synth(); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-multiple-properties.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-multiple-properties.expected.json new file mode 100644 index 0000000000000..6050ca6d243e3 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-multiple-properties.expected.json @@ -0,0 +1,261 @@ +{ + "Resources": { + "StateMachineRoleB840431D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, + "DefinitionString": "{\"StartAt\":\"PassTask\",\"States\":{\"PassTask\":{\"Type\":\"Pass\",\"Result\":\"Hello\",\"End\":true}}}", + "StateMachineType": "EXPRESS" + }, + "DependsOn": [ + "StateMachineRoleB840431D" + ] + }, + "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewDefaultPolicy6DEF984E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartSyncExecution", + "Effect": "Allow", + "Resource": { + "Ref": "StateMachine2E01A3A5" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewDefaultPolicy6DEF984E", + "Roles": [ + { + "Ref": "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0" + } + ] + } + }, + "StepFunctionsRestApiC6E3E883": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "StepFunctionsRestApi" + } + }, + "StepFunctionsRestApiCloudWatchRoleB06ACDB9": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + } + }, + "StepFunctionsRestApiAccountBD0CCC0E": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "StepFunctionsRestApiCloudWatchRoleB06ACDB9", + "Arn" + ] + } + }, + "DependsOn": [ + "StepFunctionsRestApiC6E3E883" + ] + }, + "StepFunctionsRestApiANY7699CA92": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Fn::GetAtt": [ + "StepFunctionsRestApiC6E3E883", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "StepFunctionsRestApiC6E3E883" + }, + "AuthorizationType": "NONE", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0", + "Arn" + ] + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": [ + { + "ResponseTemplates": { + "application/json": "#set($inputRoot = $input.path('$'))\n #if($input.path('$.status').toString().equals(\"FAILED\"))\n #set($context.responseOverride.status = 500)\n { \n \"error\": \"$input.path('$.error')\",\n \"cause\": \"$input.path('$.cause')\"\n }\n #else\n $input.path('$.output')\n #end" + }, + "StatusCode": "200" + }, + { + "ResponseTemplates": { + "application/json": "{\n \"error\": \"Bad input!\"\n }" + }, + "SelectionPattern": "4\\d{2}", + "StatusCode": "400" + }, + { + "ResponseTemplates": { + "application/json": "\"error\": $input.path('$.error')" + }, + "SelectionPattern": "5\\d{2}", + "StatusCode": "500" + } + ], + "PassthroughBehavior": "NEVER", + "RequestTemplates": { + "application/json": { + "Fn::Join": [ + "", + [ + "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = false)\n#set($includePath = false)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", + { + "Ref": "StateMachine2E01A3A5" + }, + "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n \n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@: @@$context.resourcePath@@\")\n #end\n \n #set($requestContext = \"@@requestContext@@: {@@accountId@@:@@$context.identity.accountId@@,@@apiId@@:@@$context.apiId@@}\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n" + ] + ] + } + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":states:action/StartSyncExecution" + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseModels": { + "application/json": "Empty" + }, + "StatusCode": "200" + }, + { + "ResponseModels": { + "application/json": "Error" + }, + "StatusCode": "400" + }, + { + "ResponseModels": { + "application/json": "Error" + }, + "StatusCode": "500" + } + ] + } + }, + "deployment33381975b5dafda9a97138f301ea25da405640e8": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "StepFunctionsRestApiC6E3E883" + } + }, + "DependsOn": [ + "StepFunctionsRestApiANY7699CA92" + ] + }, + "stage0661E4AC": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "StepFunctionsRestApiC6E3E883" + }, + "DeploymentId": { + "Ref": "deployment33381975b5dafda9a97138f301ea25da405640e8" + }, + "StageName": "prod" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-multiple-properties.ts b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-multiple-properties.ts new file mode 100644 index 0000000000000..f3b8629509675 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-multiple-properties.ts @@ -0,0 +1,45 @@ +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as apigw from '../lib'; + +/** + * Stack verification steps: + * * `curl -X POST 'https://.execute-api..amazonaws.com/prod' \ + * * -d '{"key":"Hello"}' -H 'Content-Type: application/json'` + * The above should return a "Hello" response + */ + +class StepFunctionsRestApiDeploymentStack extends cdk.Stack { + constructor(scope: Construct) { + super(scope, 'StepFunctionsRestApiDeploymentStack'); + + const passTask = new sfn.Pass(this, 'PassTask', { + result: { value: 'Hello' }, + }); + + const stateMachine = new sfn.StateMachine(this, 'StateMachine', { + definition: passTask, + stateMachineType: sfn.StateMachineType.EXPRESS, + }); + + const api = new apigw.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { + deploy: false, + stateMachine: stateMachine, + requestContext: { + accountId: true, + apiId: true, + }, + }); + + api.deploymentStage = new apigw.Stage(this, 'stage', { + deployment: new apigw.Deployment(this, 'deployment', { + api, + }), + }); + } +} + +const app = new cdk.App(); +new StepFunctionsRestApiDeploymentStack(app); +app.synth(); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-single-property.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-single-property.expected.json new file mode 100644 index 0000000000000..4448fdbc89ff8 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-single-property.expected.json @@ -0,0 +1,261 @@ +{ + "Resources": { + "StateMachineRoleB840431D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, + "DefinitionString": "{\"StartAt\":\"PassTask\",\"States\":{\"PassTask\":{\"Type\":\"Pass\",\"Result\":\"Hello\",\"End\":true}}}", + "StateMachineType": "EXPRESS" + }, + "DependsOn": [ + "StateMachineRoleB840431D" + ] + }, + "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewDefaultPolicy6DEF984E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartSyncExecution", + "Effect": "Allow", + "Resource": { + "Ref": "StateMachine2E01A3A5" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewDefaultPolicy6DEF984E", + "Roles": [ + { + "Ref": "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0" + } + ] + } + }, + "StepFunctionsRestApiC6E3E883": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "StepFunctionsRestApi" + } + }, + "StepFunctionsRestApiCloudWatchRoleB06ACDB9": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + } + }, + "StepFunctionsRestApiAccountBD0CCC0E": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "StepFunctionsRestApiCloudWatchRoleB06ACDB9", + "Arn" + ] + } + }, + "DependsOn": [ + "StepFunctionsRestApiC6E3E883" + ] + }, + "StepFunctionsRestApiANY7699CA92": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Fn::GetAtt": [ + "StepFunctionsRestApiC6E3E883", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "StepFunctionsRestApiC6E3E883" + }, + "AuthorizationType": "NONE", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0", + "Arn" + ] + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": [ + { + "ResponseTemplates": { + "application/json": "#set($inputRoot = $input.path('$'))\n #if($input.path('$.status').toString().equals(\"FAILED\"))\n #set($context.responseOverride.status = 500)\n { \n \"error\": \"$input.path('$.error')\",\n \"cause\": \"$input.path('$.cause')\"\n }\n #else\n $input.path('$.output')\n #end" + }, + "StatusCode": "200" + }, + { + "ResponseTemplates": { + "application/json": "{\n \"error\": \"Bad input!\"\n }" + }, + "SelectionPattern": "4\\d{2}", + "StatusCode": "400" + }, + { + "ResponseTemplates": { + "application/json": "\"error\": $input.path('$.error')" + }, + "SelectionPattern": "5\\d{2}", + "StatusCode": "500" + } + ], + "PassthroughBehavior": "NEVER", + "RequestTemplates": { + "application/json": { + "Fn::Join": [ + "", + [ + "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = false)\n#set($includePath = false)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", + { + "Ref": "StateMachine2E01A3A5" + }, + "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n \n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@: @@$context.resourcePath@@\")\n #end\n \n #set($requestContext = \"@@requestContext@@: {@@accountId@@:@@$context.identity.accountId@@}\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n" + ] + ] + } + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":states:action/StartSyncExecution" + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseModels": { + "application/json": "Empty" + }, + "StatusCode": "200" + }, + { + "ResponseModels": { + "application/json": "Error" + }, + "StatusCode": "400" + }, + { + "ResponseModels": { + "application/json": "Error" + }, + "StatusCode": "500" + } + ] + } + }, + "deployment33381975b5dafda9a97138f301ea25da405640e8": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "StepFunctionsRestApiC6E3E883" + } + }, + "DependsOn": [ + "StepFunctionsRestApiANY7699CA92" + ] + }, + "stage0661E4AC": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "StepFunctionsRestApiC6E3E883" + }, + "DeploymentId": { + "Ref": "deployment33381975b5dafda9a97138f301ea25da405640e8" + }, + "StageName": "prod" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-single-property.ts b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-single-property.ts new file mode 100644 index 0000000000000..95e3810aa5c77 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-single-property.ts @@ -0,0 +1,44 @@ +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as apigw from '../lib'; + +/** + * Stack verification steps: + * * `curl -X POST 'https://.execute-api..amazonaws.com/prod' \ + * * -d '{"key":"Hello"}' -H 'Content-Type: application/json'` + * The above should return a "Hello" response + */ + +class StepFunctionsRestApiDeploymentStack extends cdk.Stack { + constructor(scope: Construct) { + super(scope, 'StepFunctionsRestApiDeploymentStack'); + + const passTask = new sfn.Pass(this, 'PassTask', { + result: { value: 'Hello' }, + }); + + const stateMachine = new sfn.StateMachine(this, 'StateMachine', { + definition: passTask, + stateMachineType: sfn.StateMachineType.EXPRESS, + }); + + const api = new apigw.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { + deploy: false, + stateMachine: stateMachine, + requestContext: { + accountId: true, + }, + }); + + api.deploymentStage = new apigw.Stage(this, 'stage', { + deployment: new apigw.Deployment(this, 'deployment', { + api, + }), + }); + } +} + +const app = new cdk.App(); +new StepFunctionsRestApiDeploymentStack(app); +app.synth(); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.expected.json index f77327e40f83a..091dbb0e89817 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.expected.json @@ -44,7 +44,7 @@ "StateMachineRoleB840431D" ] }, - "StepFunctionsRestApiDeploymentStackStateMachineapiRoleE9B057CB": { + "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -61,7 +61,7 @@ } } }, - "AllowStartSyncExecutionE0A8041C": { + "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewDefaultPolicy6DEF984E": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -76,10 +76,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "AllowStartSyncExecutionE0A8041C", + "PolicyName": "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewDefaultPolicy6DEF984E", "Roles": [ { - "Ref": "StepFunctionsRestApiDeploymentStackStateMachineapiRoleE9B057CB" + "Ref": "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0" } ] } @@ -152,7 +152,7 @@ "Integration": { "Credentials": { "Fn::GetAtt": [ - "StepFunctionsRestApiDeploymentStackStateMachineapiRoleE9B057CB", + "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0", "Arn" ] }, @@ -185,11 +185,11 @@ "Fn::Join": [ "", [ - "\n #set($inputRoot = $input.path('$')) {\n \"input\": \"$util.escapeJavaScript($input.json('$'))\",\n \"stateMachineArn\": \"", + "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = false)\n#set($includePath = false)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", { "Ref": "StateMachine2E01A3A5" }, - "\"\n }" + "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n \n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@: @@$context.resourcePath@@\")\n #end\n \n #set($requestContext = \"\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n" ] ] } diff --git a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts index ee1ee31c1d06f..ede273274158d 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts @@ -108,11 +108,11 @@ describe('StepFunctions', () => { 'Fn::Join': [ '', [ - "\n #set($inputRoot = $input.path('$')) {\n \"input\": \"$util.escapeJavaScript($input.json('$'))\",\n \"stateMachineArn\": \"", + "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = false)\n#set($includePath = false)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", { Ref: 'StateMachine2E01A3A5', }, - '"\n }', + "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n \n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@: @@$context.resourcePath@@\")\n #end\n \n #set($requestContext = \"\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n", ], ], }, @@ -174,11 +174,11 @@ describe('StepFunctions', () => { 'Fn::Join': [ '', [ - "\n #set($inputRoot = $input.path('$')) {\n \"input\": \"$util.escapeJavaScript($input.json('$'))\",\n \"stateMachineArn\": \"", + "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = false)\n#set($includePath = false)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", { Ref: 'StateMachine2E01A3A5', }, - '"\n }', + "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n \n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@: @@$context.resourcePath@@\")\n #end\n \n #set($requestContext = \"\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n", ], ], }, diff --git a/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts b/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts index aceb3a2d4b795..ec837e79acec3 100644 --- a/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts @@ -120,7 +120,7 @@ describe('Step Functions api', () => { Integration: { Credentials: { 'Fn::GetAtt': [ - 'DefaultStateMachineapiRole1F29ACEB', + 'DefaultStateMachineapiRoleNewB2BFA951', 'Arn', ], }, @@ -131,11 +131,11 @@ describe('Step Functions api', () => { 'Fn::Join': [ '', [ - "\n #set($inputRoot = $input.path('$')) {\n \"input\": \"$util.escapeJavaScript($input.json('$'))\",\n \"stateMachineArn\": \"", + "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = false)\n#set($includePath = false)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", { Ref: 'StateMachine2E01A3A5', }, - '"\n }', + "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n \n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@: @@$context.resourcePath@@\")\n #end\n \n #set($requestContext = \"\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n", ], ], }, @@ -213,7 +213,7 @@ describe('Step Functions api', () => { Integration: { Credentials: { 'Fn::GetAtt': [ - 'DefaultStateMachineapiRole1F29ACEB', + 'DefaultStateMachineapiRoleNewB2BFA951', 'Arn', ], }, @@ -224,11 +224,11 @@ describe('Step Functions api', () => { 'Fn::Join': [ '', [ - "\n #set($allParams = $input.params())\n {\n \"input\": \"{\\\"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\\\"}}\",\n \"stateMachineArn\": \"", + "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = false)\n#set($includePath = false)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", { Ref: 'StateMachine2E01A3A5', }, - '"\n }', + "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n \n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@: @@$context.resourcePath@@\")\n #end\n \n #set($requestContext = \"@@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@@}\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n", ], ], }, @@ -255,6 +255,78 @@ describe('Step Functions api', () => { }); }); + test('StepFunctionsRestApi defines correct REST API resouces with includeRequestContext set to empty', () => { //GIVEN + const { stack, stateMachine } = givenSetup(); + + //WHEN + const api = new apigw.StepFunctionsRestApi(stack, + 'StepFunctionsRestApi', { + stateMachine: stateMachine, + requestContext: {}, + }); + + expect(() => { + api.root.addResource('not allowed'); + }).toThrow(); + + //THEN + expect(stack).toHaveResource('AWS::ApiGateway::Method', { + HttpMethod: 'ANY', + MethodResponses: getMethodResponse(), + AuthorizationType: 'NONE', + RestApiId: { + Ref: 'StepFunctionsRestApiC6E3E883', + }, + ResourceId: { + 'Fn::GetAtt': [ + 'StepFunctionsRestApiC6E3E883', + 'RootResourceId', + ], + }, + Integration: { + Credentials: { + 'Fn::GetAtt': [ + 'DefaultStateMachineapiRoleNewB2BFA951', + 'Arn', + ], + }, + IntegrationHttpMethod: 'POST', + IntegrationResponses: getIntegrationResponse(), + RequestTemplates: { + 'application/json': { + 'Fn::Join': [ + '', + [ + "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = false)\n#set($includePath = false)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", + { + Ref: 'StateMachine2E01A3A5', + }, + "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n \n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@: @@$context.resourcePath@@\")\n #end\n \n #set($requestContext = \"\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n", + ], + ], + }, + }, + Type: 'AWS', + Uri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':states:action/StartSyncExecution', + ], + ], + }, + PassthroughBehavior: 'NEVER', + }, + }); + }); test('fails if options.defaultIntegration is set', () => { //GIVEN diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts index d7e7a73efa3f0..7ddd4194f6778 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts @@ -164,6 +164,18 @@ abstract class StateMachineBase extends Resource implements IStateMachine { }); } + /** + * Grant the given identity permissions to start a synchronous execution of + * this state machine. + */ + public grantStartSyncExecution(identity: iam.IGrantable): iam.Grant { + return iam.Grant.addToPrincipal({ + grantee: identity, + actions: ['states:StartSyncExecution'], + resourceArns: [this.stateMachineArn], + }); + } + /** * Grant the given identity permissions to read results from state * machine. @@ -505,6 +517,14 @@ export interface IStateMachine extends IResource, iam.IGrantable { */ grantStartExecution(identity: iam.IGrantable): iam.Grant; + /** + * Grant the given identity permissions to start a synchronous execution of + * this state machine. + * + * @param identity The principal + */ + grantStartSyncExecution(identity: iam.IGrantable): iam.Grant; + /** * Grant the given identity read permissions for this state machine * diff --git a/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts index 528b79b59dd58..68d60beba6c07 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts @@ -204,6 +204,40 @@ describe('State Machine Resources', () => { }), + test('Created state machine can grant start execution to a role', () => { + // GIVEN + const stack = new cdk.Stack(); + const task = new stepfunctions.Task(stack, 'Task', { + task: { + bind: () => ({ resourceArn: 'resource' }), + }, + }); + const stateMachine = new stepfunctions.StateMachine(stack, 'StateMachine', { + definition: task, + stateMachineType: stepfunctions.StateMachineType.EXPRESS, + }); + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + + // WHEN + stateMachine.grantStartSyncExecution(role); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith(objectLike({ + Action: 'states:StartSyncExecution', + Effect: 'Allow', + Resource: { + Ref: 'StateMachine2E01A3A5', + }, + })), + }, + }); + + }), + test('Created state machine can grant read access to a role', () => { // GIVEN const stack = new cdk.Stack(); From 50680d553eff080a4c2118ab14e4265382dbf3ed Mon Sep 17 00:00:00 2001 From: Diego Santiviago Date: Mon, 22 Nov 2021 20:37:10 -0800 Subject: [PATCH 07/35] Update readme, removed cors check, fixed integrations --- packages/@aws-cdk/aws-apigateway/README.md | 57 +++++--- .../lib/integrations/request-context.ts | 81 ++++++++--- .../lib/integrations/stepfunctions.ts | 136 ++++++++++-------- .../aws-apigateway/lib/stepfunctions-api.ts | 65 +++------ ...stepfunctions-api-all-params.expected.json | 14 +- .../integ.stepfunctions-api-all-params.ts | 2 +- ...ns-api-request-context-empty.expected.json | 16 +-- ...-context-multiple-properties.expected.json | 16 +-- ...uest-context-single-property.expected.json | 16 +-- .../integ.stepfunctions-api.expected.json | 16 +-- .../test/integrations/stepfunctions.test.ts | 8 +- .../test/stepfunctions-api.test.ts | 4 +- 12 files changed, 242 insertions(+), 189 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index ab69920aa53d4..bae0cdb2bab38 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -111,25 +111,34 @@ item.addMethod('DELETE', new apigateway.HttpIntegration('http://amazon.com')); You can use Amazon API Gateway with AWS Step Functions as the backend integration, specifically Synchronous Express Workflows. -The `StepFunctionsRestApi` only supports integration with Synchronous Express state machine. The `StepFunctionsRestApi` construct makes this easy and also sets up input, output and error mapping. The `StepFunctionsRestApi` construct sets up the API Gateway REST API with an `ANY` HTTP method and sets up the api role with the required permission to invoke `StartSyncExecution` action on the AWS StepFunctions state machine. It sets up up a `prod` stage by default. This will enable you to invoke any of the API Gateway HTTP methods for that `prod` and get a response from the backend AWS Step Functions execution. Invoking either GET or POST in the example below will send the request to the state machine as a new execution. On success, an HTTP code '200' is returned with ONLY the execution output as the Response Body. If the state machine execution fails, an HTTP '500' error response is returned with the error and cause of the execution as the Response Body. If the request is invalid (ex. bad execution input) an HTTP '400' error is returned. +The `StepFunctionsRestApi` only supports integration with Synchronous Express state machine. The `StepFunctionsRestApi` construct makes this easy by setting up input, output and error mapping. -As part of the API Gateway integration with Step Functions, it is possible to opt-in to include requestContext, headers, path, and querystring to the execution input. By default, these paramaters are not included in order to reduce payload size sent to Step Functions. +The construct sets up an API endpoint and maps the `ANY` HTTP method and any calls to the API endpoint starts an express workflow execution for the underlying state machine. + +Invoking either `GET` or `POST` in the example below will send the request to the state machine as a new execution. On success, an HTTP code `200` is returned with the execution output as the Response Body. + +If the execution fails, an HTTP `500` response is returned with the `error` and `cause` from the execution output as the Response Body. If the request is invalid (ex. bad execution input) HTTP code `400` is returned. + +The API response is mapped to the state machine execution `output` field. AWS Step Functions [StartSyncExecution](https://docs.aws.amazon.com/step-functions/latest/apireference/API_StartSyncExecution.html#API_StartSyncExecution_ResponseSyntax) response includes information about billing, AWS Account ID, resource ARNs that are not returned to the caller. In case of failures, the fields `error` and `cause` are returned as part of the response. + +By default, a `prod` stage is provisioned. + +In order to reduce the payload size sent to AWS Step Functions, `headers` are not forwarded to the Step Functions execution input. It is possible to choose whether `headers`, `requestContext`, `path` and `querystring` are included or not. By default, `headers` are excluded in all requests. More details about AWS Step Functions payload limit can be found at https://docs.aws.amazon.com/step-functions/latest/dg/limits-overview.html#service-limits-task-executions. The following code defines a REST API that routes all requests to the specified AWS StepFunctions state machine: ```ts -const machineDefinition = new sfn.Pass(this, 'PassState', { - -}) +const stateMachineDefinition = new sfn.Pass(this, 'PassState'); const stateMachine: sfn.IStateMachine = new sfn.StateMachine(this, 'StateMachine', { - definition: machineDefinition, + definition: stateMachineDefinition, stateMachineType: sfn.StateMachineType.EXPRESS, }); -new apigateway.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { +new apigateway.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { + deploy: true, stateMachine: stateMachine, }); ``` @@ -138,35 +147,45 @@ Here are a few examples: Example 1: POST with default configuration -```json -POST / -{ - "customerId": 1 -} +```bash +curl -X POST -d '{ "customerId": 1 }' https://example.com/ ``` -Step Functions will wrap the request body inside a `body` key as it is possible to have other keys within the request (ex. requestContext, header, path, and/or querystring): +AWS Step Functions will receive the request body inside a `body` key as it is possible to have other keys within the request (ex. requestContext, header, path, and/or querystring): ```json { "body": { "customerId": 1 - } + }, + "path": "/", + "querystring": {} } ``` -Example2: GET with API-Gateway path enabled. Set up API Gateway with resources `/user/{userId}`. Invoking 'https://\.execute-api.\.amazonaws.com/prod/users/1'. +Example 2: GET with path `/users/{userId}`. -Request: +```bash +curl -X GET https://example.com/users/{userId} +``` + +AWS Step Functions will receive the following execution input: ```json { "body": {}, - "path": "/users/{userid}", + "path": "/users/{userId}", + "querystring": {} } ``` -Example 3: GET with API-Gateway requestContext, header, and querystring enabled. Invoking 'https://\.execute-api.\.amazonaws.com/prod?customerId=1'. +Example 3: GET with `headers`, `querystring` and `requestContext` enabled. + +```bash +curl https://example.com/?customerId=1 +``` + +AWS Step Functions will receive the following execution input: Request: @@ -182,7 +201,7 @@ Request: "CloudFront-Forwarded-Proto": "...", }, "querystring": { - "customerId": 1 + "customerId": "1" } } ``` diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/request-context.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/request-context.ts index f9f80ff7563b3..33065bba2a20d 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/request-context.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/request-context.ts @@ -1,111 +1,154 @@ /** - * Request Context interface + * This interface exposes what properties should be included in the `requestContext` + * + * More details can be found at mapping templates documentation + * + * https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html */ export interface RequestContext { /** - * Account ID string + * Represents the information of $context.identity.accountId + * + * The AWS account ID associated with the request. * @default false */ readonly accountId?: boolean; /** - * Api ID string + * Represents the information of $context.apiId + * + * The identifier API Gateway assigns to your API. * @default false */ readonly apiId?: boolean; /** - * Api Key string + * Represents the information of $context.identity.apiKey + * + * For API methods that require an API key, this variable is the API key associated with the method request. + * For methods that don't require an API key, this variable is null. * @default false */ readonly apiKey?: boolean; /** - * Authorizer Principal ID string + * Represents the information of $context.authorizer.principalId + * + * The principal user identification associated with the token sent by the client and returned from an API Gateway Lambda authorizer (formerly known as a custom authorizer) * @default false */ readonly authorizerPrincipalId?: boolean; /** - * Caller string + * Represents the information of $context.identity.caller + * + * The principal identifier of the caller that signed the request. Supported for resources that use IAM authorization. * @default false */ readonly caller?: boolean; /** - * Cognito Authentication Provider string + * Represents the information of $context.identity.cognitoAuthenticationProvider + * + * A comma-separated list of the Amazon Cognito authentication providers used by the caller making the request. Available only if the request was signed with Amazon Cognito credentials. * @default false */ readonly cognitoAuthenticationProvider?: boolean; /** - * Cognito Authentication Type string + * Represents the information of $context.identity.cognitoAuthenticationType + * + * The Amazon Cognito authentication type of the caller making the request. + * Available only if the request was signed with Amazon Cognito credentials. + * Possible values include authenticated for authenticated identities and unauthenticated for unauthenticated identities. * @default false */ readonly cognitoAuthenticationType?: boolean; /** - * Cognito Identity ID string + * Represents the information of $context.identity.cognitoIdentityId + * + * The Amazon Cognito identity ID of the caller making the request. Available only if the request was signed with Amazon Cognito credentials. * @default false */ readonly cognitoIdentityId?: boolean; /** - * Cognito Identity Pool ID string + * Represents the information of $context.identity.cognitoIdentityPoolId + * + * The Amazon Cognito identity pool ID of the caller making the request. Available only if the request was signed with Amazon Cognito credentials. * @default false */ readonly cognitoIdentityPoolId?: boolean; /** - * Http Method string + * Represents the information of $context.httpMethod + * + * The HTTP method used. Valid values include: DELETE, GET, HEAD, OPTIONS, PATCH, POST, and PUT. * @default false */ readonly httpMethod?: boolean; /** - * Stage string + * Represents the information of $context.stage + * + * The deployment stage of the API request (for example, Beta or Prod). * @default false */ readonly stage?: boolean; /** - * Source IP string + * Represents the information of $context.identity.sourceIp + * + * The source IP address of the immediate TCP connection making the request to API Gateway endpoint. * @default false */ readonly sourceIp?: boolean; /** - * User string + * Represents the information of $context.identity.user + * + * The principal identifier of the user that will be authorized against resource access. Supported for resources that use IAM authorization. * @default false */ readonly user?: boolean; /** - * User Agent string + * Represents the information of $context.identity.userAgent + * + * The User-Agent header of the API caller. * @default false */ readonly userAgent?: boolean; /** - * User Arn string + * Represents the information of $context.identity.userArn + * + * The Amazon Resource Name (ARN) of the effective user identified after authentication. * @default false */ readonly userArn?: boolean; /** - * Request ID string + * Represents the information of $context.requestId + * + * An ID for the request. Clients can override this request ID. * @default false */ readonly requestId?: boolean; /** - * Resource ID string + * Represents the information of $context.resourceId + * + * The identifier that API Gateway assigns to your resource. * @default false */ readonly resourceId?: boolean; /** - * Resource Path string + * Represents the information of $context.resourcePath + * + * The path to your resource. * @default false */ readonly resourcePath?: boolean; diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts index 1040c00a95ec5..e230d3a101ca6 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts @@ -8,44 +8,61 @@ import { IntegrationConfig, IntegrationOptions, PassthroughBehavior } from '../i import { Method } from '../method'; import { AwsIntegration } from './aws'; /** - * Options when configuring Step Functions integration with Rest API + * Options when configuring Step Functions synchronous integration with Rest API */ -export interface StepFunctionsIntegrationOptions extends IntegrationOptions { - /** - * Action for the Step Functions integration. The list of supported API actions can be found - * on https://docs.aws.amazon.com/step-functions/latest/apireference/API_Operations.html - * @default 'StartSyncExecution' - */ - readonly action: string; - - /** - * Check if cors is enabled - * @default false - */ - readonly corsEnabled?: boolean; +export interface StepFunctionsSynchronousIntegrationOptions extends IntegrationOptions { /** * Which details of the incoming request must be passed onto the underlying state machine, - * such as, account id, user identity, request id, etc. + * such as, account id, user identity, request id, etc. The execution input will include a new key `requestContext`: + * + * { + * "body": {}, + * "requestContext": { + * "key": "value" + * } + * } * * @default - all parameters within request context will be set as false */ readonly requestContext?: RequestContext; /** - * Check if querystring is to be included inside the execution input - * @default false + * Check if querystring is to be included inside the execution input. The execution input will include a new key `queryString`: + * + * { + * "body": {}, + * "querystring": { + * "key": "value" + * } + * } + * + * @default true */ - readonly queryString?: boolean; + readonly querystring?: boolean; /** - * Check if path is to be included inside the execution input - * @default false + * Check if path is to be included inside the execution input. The execution input will include a new key `path`: + * + * { + * "body": {}, + * "path": "/" + * } + * + * @default true */ readonly path?: boolean; /** - * Check if header is to be included inside the execution input + * Check if header is to be included inside the execution input. The execution input will include a new key `headers`: + * + * { + * "body": {}, + * "headers": { + * "header1": "value", + * "header2": "value" + * } + * } * @default false */ readonly headers?: boolean; @@ -56,36 +73,31 @@ export interface StepFunctionsIntegrationOptions extends IntegrationOptions { * @example * * const stateMachine = new sfn.StateMachine(this, 'MyStateMachine', ...); - * api.addMethod('GET', new StepFunctionsIntegration(stateMachine)); + * api.addMethod('GET', new StepFunctionsSynchronousIntegration(stateMachine)); */ -export class StepFunctionsIntegration extends AwsIntegration { +export class StepFunctionsSynchronousIntegration extends AwsIntegration { private readonly stateMachine: sfn.IStateMachine; - private readonly action: string; - constructor(stateMachine: sfn.IStateMachine, options: StepFunctionsIntegrationOptions = { action: 'StartSyncExecution' }) { - - const integResponse = integrationResponse(); - const requestTemplate = requestTemplates(stateMachine, options); + constructor(stateMachine: sfn.IStateMachine, options: StepFunctionsSynchronousIntegrationOptions) { super({ service: 'states', - action: options.action, + action: 'StartSyncExecution', options: { credentialsRole: options.credentialsRole, - integrationResponses: integResponse, + integrationResponses: integrationResponse(), passthroughBehavior: PassthroughBehavior.NEVER, - requestTemplates: requestTemplate, + requestTemplates: requestTemplates(stateMachine, options), ...options, }, }); this.stateMachine = stateMachine; - this.action = options.action; } public bind(method: Method): IntegrationConfig { const bindResult = super.bind(method); const principal = new iam.ServicePrincipal('apigateway.amazonaws.com'); - this.stateMachine.grantExecution(principal, `states:${this.action}`); + this.stateMachine.grantExecution(principal, 'states:StartSyncExecution'); let stateMachineName; @@ -95,7 +107,7 @@ export class StepFunctionsIntegration extends AwsIntegration { stateMachineName = (this.stateMachine.node.defaultChild as sfn.CfnStateMachine).stateMachineName; } else { //imported state machine - stateMachineName = 'StateMachine-' + (String(this.stateMachine.stack.node.addr)); + stateMachineName = `StateMachine-${this.stateMachine.stack.node.addr}`; } let deploymentToken; @@ -107,15 +119,14 @@ export class StepFunctionsIntegration extends AwsIntegration { ...bindResult, deploymentToken, }; - } } /** - * Defines the integration response that passes the result, on success, - * or the error, on failure, from the backend the client. + * Defines the integration response that passes the result on success, + * or the error on failure, from the synchronous execution to the caller. * - * @returns integrationResponse + * @returns integrationResponse mapping */ function integrationResponse() { const errorResponse = [ @@ -129,7 +140,7 @@ function integrationResponse() { statusCode: '400', responseTemplates: { 'application/json': `{ - "error": "Bad input!" + "error": "Bad request!" }`, }, }, @@ -149,16 +160,18 @@ function integrationResponse() { { 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`, + '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', + ].join('\n'), }, }, ...errorResponse, @@ -168,7 +181,7 @@ function integrationResponse() { } /** - * Checks each property of the RequestContext Object to see if request context has + * Checks each property of the RequestContext to see if request context has * a property specified and then return true or false. * @param requestContextObj * @returns boolean @@ -199,7 +212,7 @@ function checkIncludeRequestContext(requestContextObj: RequestContext | undefine * @param options * @returns requestTemplate */ -function requestTemplates(stateMachine: sfn.IStateMachine, options: StepFunctionsIntegrationOptions) { +function requestTemplates(stateMachine: sfn.IStateMachine, options: StepFunctionsSynchronousIntegrationOptions) { let includeRequestContext: boolean = checkIncludeRequestContext(options.requestContext); const templateStr = templateString(stateMachine, includeRequestContext, options); @@ -220,27 +233,29 @@ function requestTemplates(stateMachine: sfn.IStateMachine, options: StepFunction * @param options * @reutrns templateString */ -function templateString(_stateMachine: sfn.IStateMachine, _includeRequestContext: boolean, _options: StepFunctionsIntegrationOptions): string { +function templateString( + stateMachine: sfn.IStateMachine, + includeRequestContext: boolean, + options: StepFunctionsSynchronousIntegrationOptions): string { let templateStr: string; let requestContextStr = ''; - const includeHeader: string = (_options.headers) ? 'true': 'false'; - const includeQueryString: string = (_options.queryString) ? 'true': 'false'; - const includePath: string = (_options.path) ? 'true': 'false'; + const includeHeader: string = (options.headers) ? 'true': 'false'; + const includeQueryString: string = (options.querystring) ? 'true': 'false'; + const includePath: string = (options.path) ? 'true': 'false'; - if (_includeRequestContext) { - requestContextStr = requestContext(_options.requestContext); + if (includeRequestContext) { + requestContextStr = requestContext(options.requestContext); } templateStr = fs.readFileSync(path.join(__dirname, 'stepfunctions.vtl'), { encoding: 'utf-8' }); - templateStr = templateStr.replace('%STATEMACHINE%', _stateMachine.stateMachineArn); + templateStr = templateStr.replace('%STATEMACHINE%', stateMachine.stateMachineArn); templateStr = templateStr.replace('%INCLUDE_HEADERS%', includeHeader); templateStr = templateStr.replace('%INCLUDE_QUERYSTRING%', includeQueryString); templateStr = templateStr.replace('%INCLUDE_PATH%', includePath); templateStr = templateStr.replace('%REQUESTCONTEXT%', requestContextStr); - return templateStr; } @@ -309,6 +324,5 @@ function requestContext(requestContextObj: RequestContext | undefined): string { const search = '"'; const replaceWith = '@@'; - const requestContextStrModified = requestContextStr.split(search).join(replaceWith); - return requestContextStrModified; + return requestContextStr.split(search).join(replaceWith); } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts b/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts index 2479b8c47d939..24783e7e876a8 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts @@ -3,7 +3,7 @@ import * as sfn from '@aws-cdk/aws-stepfunctions'; import { Construct } from 'constructs'; import { RestApi, RestApiProps } from '.'; import { RequestContext } from './integrations'; -import { StepFunctionsIntegration } from './integrations/stepfunctions'; +import { StepFunctionsSynchronousIntegration } from './integrations/stepfunctions'; import { Model } from './model'; /** @@ -33,13 +33,13 @@ export interface StepFunctionsRestApiProps extends RestApiProps { /** * Check if querystring is to be included inside the execution input - * @default false + * @default true */ - readonly queryString?: boolean; + readonly querystring?: boolean; /** * Check if path is to be included inside the execution input - * @default false + * @default true */ readonly path?: boolean; @@ -63,54 +63,35 @@ export class StepFunctionsRestApi extends RestApi { throw new Error('State Machine must be of type "EXPRESS". Please use StateMachineType.EXPRESS as the stateMachineType'); } - const apiRole = role(scope, props); - const methodResp = methodResponse(); - - let corsEnabled; + const stepfunctionsIntegration = new StepFunctionsSynchronousIntegration(props.stateMachine, { + credentialsRole: role(scope, props), + requestContext: props.requestContext, + path: props.path?? true, + querystring: props.querystring?? true, + headers: props.headers, + }); - if (props.defaultCorsPreflightOptions !== undefined) { - corsEnabled = true; - } else { - corsEnabled = false; - } + super(scope, id, props); - super(scope, id, { - defaultIntegration: new StepFunctionsIntegration(props.stateMachine, { - credentialsRole: apiRole, - corsEnabled: corsEnabled, - requestContext: props.requestContext, - action: 'StartSyncExecution', - }), - ...props, + this.root.addMethod('ANY', stepfunctionsIntegration, { + methodResponses: [ + ...methodResponse(), + ], }); - - if (!corsEnabled) { - this.root.addMethod('ANY', new StepFunctionsIntegration(props.stateMachine, { - credentialsRole: apiRole, - requestContext: props.requestContext, - action: 'StartSyncExecution', - queryString: props.queryString, - path: props.path, - headers: props.headers, - }), { - methodResponses: [ - ...methodResp, - ], - }); - } } } + /** - * Defines the IAM Role for API Gateway with required permisisons - * to perform action on the state machine. + * Defines the IAM Role for API Gateway with required permissions + * to invoke a synchronous execution for the provided state machine * * @param scope * @param props * @returns Role - IAM Role */ function role(scope: Construct, props: StepFunctionsRestApiProps): iam.Role { - const apiName: string = props.stateMachine + '-apiRoleNew'; - const apiRole = new iam.Role(scope, apiName, { + const roleName: string = 'StartSyncExecutionRole'; + const apiRole = new iam.Role(scope, roleName, { assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), }); @@ -124,7 +105,7 @@ function role(scope: Construct, props: StepFunctionsRestApiProps): iam.Role { * @returns methodResponse */ function methodResponse() { - const methodResp = [ + return [ { statusCode: '200', responseModels: { @@ -144,6 +125,4 @@ function methodResponse() { }, }, ]; - - return methodResp; } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-all-params.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-all-params.expected.json index 859b070be3ea5..a19dd993f108a 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-all-params.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-all-params.expected.json @@ -44,7 +44,7 @@ "StateMachineRoleB840431D" ] }, - "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0": { + "StartSyncExecutionRoleDE73CB90": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -61,7 +61,7 @@ } } }, - "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewDefaultPolicy6DEF984E": { + "StartSyncExecutionRoleDefaultPolicy5A5803F8": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -76,10 +76,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewDefaultPolicy6DEF984E", + "PolicyName": "StartSyncExecutionRoleDefaultPolicy5A5803F8", "Roles": [ { - "Ref": "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0" + "Ref": "StartSyncExecutionRoleDE73CB90" } ] } @@ -152,7 +152,7 @@ "Integration": { "Credentials": { "Fn::GetAtt": [ - "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0", + "StartSyncExecutionRoleDE73CB90", "Arn" ] }, @@ -160,13 +160,13 @@ "IntegrationResponses": [ { "ResponseTemplates": { - "application/json": "#set($inputRoot = $input.path('$'))\n #if($input.path('$.status').toString().equals(\"FAILED\"))\n #set($context.responseOverride.status = 500)\n { \n \"error\": \"$input.path('$.error')\",\n \"cause\": \"$input.path('$.cause')\"\n }\n #else\n $input.path('$.output')\n #end" + "application/json": "#set($inputRoot = $input.path('$'))\n#if($input.path('$.status').toString().equals(\"FAILED\"))\n#set($context.responseOverride.status = 500)\n{\n\"error\": \"$input.path('$.error')\"\n\"cause\": \"$input.path('$.cause')\"\n}\n#else\n$input.path('$.output')\n#end" }, "StatusCode": "200" }, { "ResponseTemplates": { - "application/json": "{\n \"error\": \"Bad input!\"\n }" + "application/json": "{\n \"error\": \"Bad request!\"\n }" }, "SelectionPattern": "4\\d{2}", "StatusCode": "400" diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-all-params.ts b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-all-params.ts index 2e7d99b0eb15b..215db9f7f053b 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-all-params.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-all-params.ts @@ -27,7 +27,7 @@ class StepFunctionsRestApiDeploymentStack extends cdk.Stack { deploy: false, stateMachine: stateMachine, headers: true, - queryString: true, + querystring: true, path: true, requestContext: { accountId: true, diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-empty.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-empty.expected.json index 091dbb0e89817..51d4c989e9564 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-empty.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-empty.expected.json @@ -44,7 +44,7 @@ "StateMachineRoleB840431D" ] }, - "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0": { + "StartSyncExecutionRoleDE73CB90": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -61,7 +61,7 @@ } } }, - "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewDefaultPolicy6DEF984E": { + "StartSyncExecutionRoleDefaultPolicy5A5803F8": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -76,10 +76,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewDefaultPolicy6DEF984E", + "PolicyName": "StartSyncExecutionRoleDefaultPolicy5A5803F8", "Roles": [ { - "Ref": "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0" + "Ref": "StartSyncExecutionRoleDE73CB90" } ] } @@ -152,7 +152,7 @@ "Integration": { "Credentials": { "Fn::GetAtt": [ - "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0", + "StartSyncExecutionRoleDE73CB90", "Arn" ] }, @@ -160,13 +160,13 @@ "IntegrationResponses": [ { "ResponseTemplates": { - "application/json": "#set($inputRoot = $input.path('$'))\n #if($input.path('$.status').toString().equals(\"FAILED\"))\n #set($context.responseOverride.status = 500)\n { \n \"error\": \"$input.path('$.error')\",\n \"cause\": \"$input.path('$.cause')\"\n }\n #else\n $input.path('$.output')\n #end" + "application/json": "#set($inputRoot = $input.path('$'))\n#if($input.path('$.status').toString().equals(\"FAILED\"))\n#set($context.responseOverride.status = 500)\n{\n\"error\": \"$input.path('$.error')\"\n\"cause\": \"$input.path('$.cause')\"\n}\n#else\n$input.path('$.output')\n#end" }, "StatusCode": "200" }, { "ResponseTemplates": { - "application/json": "{\n \"error\": \"Bad input!\"\n }" + "application/json": "{\n \"error\": \"Bad request!\"\n }" }, "SelectionPattern": "4\\d{2}", "StatusCode": "400" @@ -185,7 +185,7 @@ "Fn::Join": [ "", [ - "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = false)\n#set($includePath = false)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", + "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = true)\n#set($includePath = true)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", { "Ref": "StateMachine2E01A3A5" }, diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-multiple-properties.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-multiple-properties.expected.json index 6050ca6d243e3..205c382d7198e 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-multiple-properties.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-multiple-properties.expected.json @@ -44,7 +44,7 @@ "StateMachineRoleB840431D" ] }, - "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0": { + "StartSyncExecutionRoleDE73CB90": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -61,7 +61,7 @@ } } }, - "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewDefaultPolicy6DEF984E": { + "StartSyncExecutionRoleDefaultPolicy5A5803F8": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -76,10 +76,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewDefaultPolicy6DEF984E", + "PolicyName": "StartSyncExecutionRoleDefaultPolicy5A5803F8", "Roles": [ { - "Ref": "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0" + "Ref": "StartSyncExecutionRoleDE73CB90" } ] } @@ -152,7 +152,7 @@ "Integration": { "Credentials": { "Fn::GetAtt": [ - "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0", + "StartSyncExecutionRoleDE73CB90", "Arn" ] }, @@ -160,13 +160,13 @@ "IntegrationResponses": [ { "ResponseTemplates": { - "application/json": "#set($inputRoot = $input.path('$'))\n #if($input.path('$.status').toString().equals(\"FAILED\"))\n #set($context.responseOverride.status = 500)\n { \n \"error\": \"$input.path('$.error')\",\n \"cause\": \"$input.path('$.cause')\"\n }\n #else\n $input.path('$.output')\n #end" + "application/json": "#set($inputRoot = $input.path('$'))\n#if($input.path('$.status').toString().equals(\"FAILED\"))\n#set($context.responseOverride.status = 500)\n{\n\"error\": \"$input.path('$.error')\"\n\"cause\": \"$input.path('$.cause')\"\n}\n#else\n$input.path('$.output')\n#end" }, "StatusCode": "200" }, { "ResponseTemplates": { - "application/json": "{\n \"error\": \"Bad input!\"\n }" + "application/json": "{\n \"error\": \"Bad request!\"\n }" }, "SelectionPattern": "4\\d{2}", "StatusCode": "400" @@ -185,7 +185,7 @@ "Fn::Join": [ "", [ - "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = false)\n#set($includePath = false)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", + "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = true)\n#set($includePath = true)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", { "Ref": "StateMachine2E01A3A5" }, diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-single-property.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-single-property.expected.json index 4448fdbc89ff8..1ac39fd9b121c 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-single-property.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-single-property.expected.json @@ -44,7 +44,7 @@ "StateMachineRoleB840431D" ] }, - "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0": { + "StartSyncExecutionRoleDE73CB90": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -61,7 +61,7 @@ } } }, - "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewDefaultPolicy6DEF984E": { + "StartSyncExecutionRoleDefaultPolicy5A5803F8": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -76,10 +76,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewDefaultPolicy6DEF984E", + "PolicyName": "StartSyncExecutionRoleDefaultPolicy5A5803F8", "Roles": [ { - "Ref": "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0" + "Ref": "StartSyncExecutionRoleDE73CB90" } ] } @@ -152,7 +152,7 @@ "Integration": { "Credentials": { "Fn::GetAtt": [ - "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0", + "StartSyncExecutionRoleDE73CB90", "Arn" ] }, @@ -160,13 +160,13 @@ "IntegrationResponses": [ { "ResponseTemplates": { - "application/json": "#set($inputRoot = $input.path('$'))\n #if($input.path('$.status').toString().equals(\"FAILED\"))\n #set($context.responseOverride.status = 500)\n { \n \"error\": \"$input.path('$.error')\",\n \"cause\": \"$input.path('$.cause')\"\n }\n #else\n $input.path('$.output')\n #end" + "application/json": "#set($inputRoot = $input.path('$'))\n#if($input.path('$.status').toString().equals(\"FAILED\"))\n#set($context.responseOverride.status = 500)\n{\n\"error\": \"$input.path('$.error')\"\n\"cause\": \"$input.path('$.cause')\"\n}\n#else\n$input.path('$.output')\n#end" }, "StatusCode": "200" }, { "ResponseTemplates": { - "application/json": "{\n \"error\": \"Bad input!\"\n }" + "application/json": "{\n \"error\": \"Bad request!\"\n }" }, "SelectionPattern": "4\\d{2}", "StatusCode": "400" @@ -185,7 +185,7 @@ "Fn::Join": [ "", [ - "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = false)\n#set($includePath = false)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", + "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = true)\n#set($includePath = true)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", { "Ref": "StateMachine2E01A3A5" }, diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.expected.json index 091dbb0e89817..51d4c989e9564 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.expected.json @@ -44,7 +44,7 @@ "StateMachineRoleB840431D" ] }, - "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0": { + "StartSyncExecutionRoleDE73CB90": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -61,7 +61,7 @@ } } }, - "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewDefaultPolicy6DEF984E": { + "StartSyncExecutionRoleDefaultPolicy5A5803F8": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -76,10 +76,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewDefaultPolicy6DEF984E", + "PolicyName": "StartSyncExecutionRoleDefaultPolicy5A5803F8", "Roles": [ { - "Ref": "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0" + "Ref": "StartSyncExecutionRoleDE73CB90" } ] } @@ -152,7 +152,7 @@ "Integration": { "Credentials": { "Fn::GetAtt": [ - "StepFunctionsRestApiDeploymentStackStateMachineapiRoleNewD8D444E0", + "StartSyncExecutionRoleDE73CB90", "Arn" ] }, @@ -160,13 +160,13 @@ "IntegrationResponses": [ { "ResponseTemplates": { - "application/json": "#set($inputRoot = $input.path('$'))\n #if($input.path('$.status').toString().equals(\"FAILED\"))\n #set($context.responseOverride.status = 500)\n { \n \"error\": \"$input.path('$.error')\",\n \"cause\": \"$input.path('$.cause')\"\n }\n #else\n $input.path('$.output')\n #end" + "application/json": "#set($inputRoot = $input.path('$'))\n#if($input.path('$.status').toString().equals(\"FAILED\"))\n#set($context.responseOverride.status = 500)\n{\n\"error\": \"$input.path('$.error')\"\n\"cause\": \"$input.path('$.cause')\"\n}\n#else\n$input.path('$.output')\n#end" }, "StatusCode": "200" }, { "ResponseTemplates": { - "application/json": "{\n \"error\": \"Bad input!\"\n }" + "application/json": "{\n \"error\": \"Bad request!\"\n }" }, "SelectionPattern": "4\\d{2}", "StatusCode": "400" @@ -185,7 +185,7 @@ "Fn::Join": [ "", [ - "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = false)\n#set($includePath = false)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", + "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = true)\n#set($includePath = true)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", { "Ref": "StateMachine2E01A3A5" }, diff --git a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts index ede273274158d..062e7cd66c4c8 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts @@ -67,7 +67,7 @@ describe('StepFunctions', () => { const { stack, api, stateMachine } = givenSetup(); //WHEN - const integ = new apigw.StepFunctionsIntegration(stateMachine); + const integ = new apigw.StepFunctionsSynchronousIntegration(stateMachine, {}); api.root.addMethod('GET', integ); //THEN @@ -137,7 +137,7 @@ describe('StepFunctions', () => { stateMachineType: sfn.StateMachineType.EXPRESS, }); - api.root.addMethod('ANY', new apigw.StepFunctionsIntegration(stateMachine)); + api.root.addMethod('ANY', new apigw.StepFunctionsSynchronousIntegration(stateMachine, {})); expect(stack).toHaveResource('AWS::ApiGateway::Method', { HttpMethod: 'ANY', @@ -219,7 +219,7 @@ describe('StepFunctions', () => { stateMachineType: sfn.StateMachineType.EXPRESS, }); - const integ = new apigw.StepFunctionsIntegration(stateMachine); + const integ = new apigw.StepFunctionsSynchronousIntegration(stateMachine, {}); // WHEN const bindResult = integ.bind(method); @@ -234,7 +234,7 @@ describe('StepFunctions', () => { const restapi = new apigw.RestApi(stack, 'RestApi'); const method = restapi.root.addMethod('ANY'); const stateMachine: sfn.IStateMachine = StateMachine.fromStateMachineArn(stack, 'MyStateMachine', 'arn:aws:states:region:account:stateMachine:MyStateMachine'); - const integration = new apigw.StepFunctionsIntegration(stateMachine); + const integration = new apigw.StepFunctionsSynchronousIntegration(stateMachine, {}); // WHEN const bindResult = integration.bind(method); diff --git a/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts b/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts index ec837e79acec3..4b077ce1816b7 100644 --- a/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts @@ -25,7 +25,7 @@ function whenCondition(stack:cdk.Stack, stateMachine: sfn.IStateMachine) { } function getMethodResponse() { - const methodResp = [ + return [ { StatusCode: '200', ResponseModels: { @@ -45,8 +45,6 @@ function getMethodResponse() { }, }, ]; - - return methodResp; } function getIntegrationResponse() { From a6b816510805c5090937a19da33093618b3bf3d6 Mon Sep 17 00:00:00 2001 From: Diego Santiviago Date: Mon, 22 Nov 2021 22:01:48 -0800 Subject: [PATCH 08/35] Update test signature - changed interface --- .../aws-stepfunctions/test/state-machine-resources.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts index f436f5abfd41a..92bd59e3fcf28 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts @@ -216,15 +216,15 @@ describe('State Machine Resources', () => { stateMachine.grantStartSyncExecution(role); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith(objectLike({ + Statement: Match.arrayWith([Match.objectLike({ Action: 'states:StartSyncExecution', Effect: 'Allow', Resource: { Ref: 'StateMachine2E01A3A5', }, - })), + })]), }, }); From 10e361f9086ab951b23d5f34dd8dad2791ae1136 Mon Sep 17 00:00:00 2001 From: Diego Santiviago Date: Mon, 22 Nov 2021 23:35:02 -0800 Subject: [PATCH 09/35] update tests to include querystring and path as default --- .../lib/integrations/stepfunctions.ts | 14 +++--- .../test/integrations/stepfunctions.test.ts | 47 ++++++++++++------- .../test/stepfunctions-api.test.ts | 37 ++++++++------- 3 files changed, 56 insertions(+), 42 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts index e230d3a101ca6..6a63ffa512e84 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts @@ -77,7 +77,7 @@ export interface StepFunctionsSynchronousIntegrationOptions extends IntegrationO */ export class StepFunctionsSynchronousIntegration extends AwsIntegration { private readonly stateMachine: sfn.IStateMachine; - constructor(stateMachine: sfn.IStateMachine, options: StepFunctionsSynchronousIntegrationOptions) { + constructor(stateMachine: sfn.IStateMachine, options: StepFunctionsSynchronousIntegrationOptions = {}) { super({ service: 'states', action: 'StartSyncExecution', @@ -241,9 +241,9 @@ function templateString( let requestContextStr = ''; - const includeHeader: string = (options.headers) ? 'true': 'false'; - const includeQueryString: string = (options.querystring) ? 'true': 'false'; - const includePath: string = (options.path) ? 'true': 'false'; + const includeHeader = options.headers?? false; + const includeQueryString = options.querystring?? true; + const includePath = options.path?? true; if (includeRequestContext) { requestContextStr = requestContext(options.requestContext); @@ -251,9 +251,9 @@ function templateString( templateStr = fs.readFileSync(path.join(__dirname, 'stepfunctions.vtl'), { encoding: 'utf-8' }); templateStr = templateStr.replace('%STATEMACHINE%', stateMachine.stateMachineArn); - templateStr = templateStr.replace('%INCLUDE_HEADERS%', includeHeader); - templateStr = templateStr.replace('%INCLUDE_QUERYSTRING%', includeQueryString); - templateStr = templateStr.replace('%INCLUDE_PATH%', includePath); + templateStr = templateStr.replace('%INCLUDE_HEADERS%', String(includeHeader)); + templateStr = templateStr.replace('%INCLUDE_QUERYSTRING%', String(includeQueryString)); + templateStr = templateStr.replace('%INCLUDE_PATH%', String(includePath)); templateStr = templateStr.replace('%REQUESTCONTEXT%', requestContextStr); return templateStr; diff --git a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts index 062e7cd66c4c8..28dd3f264ef93 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts @@ -26,7 +26,7 @@ function getIntegrationResponse() { StatusCode: '400', ResponseTemplates: { 'application/json': `{ - "error": "Bad input!" + "error": "Bad request!" }`, }, }, @@ -43,16 +43,18 @@ function getIntegrationResponse() { { 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`, + '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', + ].join('\n'), }, }, ...errorResponse, @@ -67,7 +69,7 @@ describe('StepFunctions', () => { const { stack, api, stateMachine } = givenSetup(); //WHEN - const integ = new apigw.StepFunctionsSynchronousIntegration(stateMachine, {}); + const integ = new apigw.StepFunctionsSynchronousIntegration(stateMachine); api.root.addMethod('GET', integ); //THEN @@ -108,7 +110,7 @@ describe('StepFunctions', () => { 'Fn::Join': [ '', [ - "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = false)\n#set($includePath = false)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", + "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = true)\n#set($includePath = true)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", { Ref: 'StateMachine2E01A3A5', }, @@ -137,7 +139,7 @@ describe('StepFunctions', () => { stateMachineType: sfn.StateMachineType.EXPRESS, }); - api.root.addMethod('ANY', new apigw.StepFunctionsSynchronousIntegration(stateMachine, {})); + api.root.addMethod('ANY', new apigw.StepFunctionsSynchronousIntegration(stateMachine)); expect(stack).toHaveResource('AWS::ApiGateway::Method', { HttpMethod: 'ANY', @@ -149,13 +151,24 @@ describe('StepFunctions', () => { IntegrationResponses: [ { ResponseTemplates: { - 'application/json': "#set($inputRoot = $input.path('$'))\n #if($input.path('$.status').toString().equals(\"FAILED\"))\n #set($context.responseOverride.status = 500)\n { \n \"error\": \"$input.path('$.error')\",\n \"cause\": \"$input.path('$.cause')\"\n }\n #else\n $input.path('$.output')\n #end", + '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', + ].join('\n'), }, StatusCode: '200', }, { ResponseTemplates: { - 'application/json': '{\n "error": "Bad input!"\n }', + 'application/json': '{\n "error": "Bad request!"\n }', }, SelectionPattern: '4\\d{2}', StatusCode: '400', @@ -174,7 +187,7 @@ describe('StepFunctions', () => { 'Fn::Join': [ '', [ - "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = false)\n#set($includePath = false)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", + "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = true)\n#set($includePath = true)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", { Ref: 'StateMachine2E01A3A5', }, diff --git a/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts b/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts index 4b077ce1816b7..e21d49a40c754 100644 --- a/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts @@ -54,7 +54,7 @@ function getIntegrationResponse() { StatusCode: '400', ResponseTemplates: { 'application/json': `{ - "error": "Bad input!" + "error": "Bad request!" }`, }, }, @@ -71,16 +71,18 @@ function getIntegrationResponse() { { 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`, + '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', + ].join('\n'), }, }, ...errorResponse, @@ -118,7 +120,7 @@ describe('Step Functions api', () => { Integration: { Credentials: { 'Fn::GetAtt': [ - 'DefaultStateMachineapiRoleNewB2BFA951', + 'StartSyncExecutionRoleDE73CB90', 'Arn', ], }, @@ -129,7 +131,7 @@ describe('Step Functions api', () => { 'Fn::Join': [ '', [ - "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = false)\n#set($includePath = false)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", + "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = true)\n#set($includePath = true)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", { Ref: 'StateMachine2E01A3A5', }, @@ -211,7 +213,7 @@ describe('Step Functions api', () => { Integration: { Credentials: { 'Fn::GetAtt': [ - 'DefaultStateMachineapiRoleNewB2BFA951', + 'StartSyncExecutionRoleDE73CB90', 'Arn', ], }, @@ -222,7 +224,7 @@ describe('Step Functions api', () => { 'Fn::Join': [ '', [ - "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = false)\n#set($includePath = false)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", + "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = true)\n#set($includePath = true)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", { Ref: 'StateMachine2E01A3A5', }, @@ -284,7 +286,7 @@ describe('Step Functions api', () => { Integration: { Credentials: { 'Fn::GetAtt': [ - 'DefaultStateMachineapiRoleNewB2BFA951', + 'StartSyncExecutionRoleDE73CB90', 'Arn', ], }, @@ -295,7 +297,7 @@ describe('Step Functions api', () => { 'Fn::Join': [ '', [ - "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = false)\n#set($includePath = false)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", + "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = true)\n#set($includePath = true)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", { Ref: 'StateMachine2E01A3A5', }, @@ -357,7 +359,6 @@ describe('Step Functions api', () => { expect(() => new apigw.StepFunctionsRestApi(stack, 'StepFunctionsRestApi', { stateMachine: stateMachine, })).toThrow(/State Machine must be of type "EXPRESS". Please use StateMachineType.EXPRESS as the stateMachineType/); - }); test('StepFunctionsRestApi defines a REST API with CORS enabled', () => { From 151ead77b65ba2069524c93622e7663c1b84e9d6 Mon Sep 17 00:00:00 2001 From: Diego Santiviago Date: Mon, 22 Nov 2021 23:56:28 -0800 Subject: [PATCH 10/35] Remove deprecated Step Functions Task utilization --- .../test/state-machine-resources.test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts index 92bd59e3fcf28..e80201f5d4adf 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts @@ -196,14 +196,10 @@ describe('State Machine Resources', () => { }), - test('Created state machine can grant start execution to a role', () => { + test('Created state machine can grant start sync execution to a role', () => { // GIVEN const stack = new cdk.Stack(); - const task = new stepfunctions.Task(stack, 'Task', { - task: { - bind: () => ({ resourceArn: 'resource' }), - }, - }); + const task = new FakeTask(stack, 'Task'); const stateMachine = new stepfunctions.StateMachine(stack, 'StateMachine', { definition: task, stateMachineType: stepfunctions.StateMachineType.EXPRESS, From 7e416ef923bea0e785159ee9798cd6a69e3b2df2 Mon Sep 17 00:00:00 2001 From: Diego Santiviago Date: Tue, 23 Nov 2021 02:15:07 -0800 Subject: [PATCH 11/35] update fixtures to include step functions and updated examples --- packages/@aws-cdk/aws-apigateway/README.md | 6 +++--- .../aws-apigateway/lib/integrations/stepfunctions.ts | 10 ++++++++-- packages/@aws-cdk/aws-apigateway/package.json | 4 ++-- .../@aws-cdk/aws-apigateway/rosetta/default.ts-fixture | 1 + packages/@aws-cdk/aws-stepfunctions/README.md | 8 ++++---- 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index bae0cdb2bab38..6bb435f78ff47 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -130,11 +130,11 @@ More details about AWS Step Functions payload limit can be found at https://docs The following code defines a REST API that routes all requests to the specified AWS StepFunctions state machine: ```ts -const stateMachineDefinition = new sfn.Pass(this, 'PassState'); +const stateMachineDefinition = new stepfunctions.Pass(this, 'PassState'); -const stateMachine: sfn.IStateMachine = new sfn.StateMachine(this, 'StateMachine', { +const stateMachine: stepfunctions.IStateMachine = new stepfunctions.StateMachine(this, 'StateMachine', { definition: stateMachineDefinition, - stateMachineType: sfn.StateMachineType.EXPRESS, + stateMachineType: stepfunctions.StateMachineType.EXPRESS, }); new apigateway.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts index 6a63ffa512e84..a9adaf7af80c1 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts @@ -72,8 +72,14 @@ export interface StepFunctionsSynchronousIntegrationOptions extends IntegrationO * * @example * - * const stateMachine = new sfn.StateMachine(this, 'MyStateMachine', ...); - * api.addMethod('GET', new StepFunctionsSynchronousIntegration(stateMachine)); + * const stateMachine = new stepfunctions.StateMachine(this, 'MyStateMachine', { + * definition: stepfunctions.Chain.start(new stepfunctions.Pass(this, 'Pass')), + * }); + * + * const api = new apigateway.RestApi(this, 'Api', { + * restApiName: 'MyApi', + * }); + * api.root.addMethod('GET', new apigateway.StepFunctionsSynchronousIntegration(stateMachine)); */ export class StepFunctionsSynchronousIntegration extends AwsIntegration { private readonly stateMachine: sfn.IStateMachine; diff --git a/packages/@aws-cdk/aws-apigateway/package.json b/packages/@aws-cdk/aws-apigateway/package.json index 5755eb5e6e050..79a863f75a9b5 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -97,9 +97,9 @@ "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", + "@aws-cdk/aws-stepfunctions": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/cx-api": "0.0.0", - "@aws-cdk/aws-stepfunctions": "0.0.0", "constructs": "^3.3.69" }, "homepage": "https://github.com/aws/aws-cdk", @@ -114,9 +114,9 @@ "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", + "@aws-cdk/aws-stepfunctions": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/cx-api": "0.0.0", - "@aws-cdk/aws-stepfunctions": "0.0.0", "constructs": "^3.3.69" }, "engines": { diff --git a/packages/@aws-cdk/aws-apigateway/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-apigateway/rosetta/default.ts-fixture index 54f81e82e4460..d019e03bd0acd 100644 --- a/packages/@aws-cdk/aws-apigateway/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/aws-apigateway/rosetta/default.ts-fixture @@ -7,6 +7,7 @@ import iam = require('@aws-cdk/aws-iam'); import s3 = require('@aws-cdk/aws-s3'); import ec2 = require('@aws-cdk/aws-ec2'); import logs = require('@aws-cdk/aws-logs'); +import stepfunctions = require('@aws-cdk/aws-stepfunctions'); class Fixture extends Stack { constructor(scope: Construct, id: string) { diff --git a/packages/@aws-cdk/aws-stepfunctions/README.md b/packages/@aws-cdk/aws-stepfunctions/README.md index e74367df84676..b57f73ac7562a 100644 --- a/packages/@aws-cdk/aws-stepfunctions/README.md +++ b/packages/@aws-cdk/aws-stepfunctions/README.md @@ -623,8 +623,8 @@ import * as logs from '@aws-cdk/aws-logs'; const logGroup = new logs.LogGroup(this, 'MyLogGroup'); -new sfn.StateMachine(this, 'MyStateMachine', { - definition: sfn.Chain.start(new sfn.Pass(this, 'Pass')), +new stepfunctions.StateMachine(this, 'MyStateMachine', { + definition: stepfunctions.Chain.start(new sfn.Pass(this, 'Pass')), logs: { destination: logGroup, level: sfn.LogLevel.ALL, @@ -637,8 +637,8 @@ new sfn.StateMachine(this, 'MyStateMachine', { Enable X-Ray tracing for StateMachine: ```ts -new sfn.StateMachine(this, 'MyStateMachine', { - definition: sfn.Chain.start(new sfn.Pass(this, 'Pass')), +new stepfunctions.StateMachine(this, 'MyStateMachine', { + definition: stepfunctions.Chain.start(new stepfunctions.Pass(this, 'Pass')), tracingEnabled: true, }); ``` From ab86c7efafa52956e07fd6496e6aaf286429b89f Mon Sep 17 00:00:00 2001 From: diegotry <33488603+diegotry@users.noreply.github.com> Date: Tue, 23 Nov 2021 09:40:01 -0800 Subject: [PATCH 12/35] Update packages/@aws-cdk/aws-apigateway/README.md Co-authored-by: Niranjan Jayakar --- packages/@aws-cdk/aws-apigateway/README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 6bb435f78ff47..faf99adfd7393 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -143,9 +143,7 @@ new apigateway.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { }); ``` -Here are a few examples: - -Example 1: POST with default configuration +When the REST API endpoint configuration above is invoked using POST, as follows - ```bash curl -X POST -d '{ "customerId": 1 }' https://example.com/ From 5306f22675f82eb811e486ab88c4107c2e135014 Mon Sep 17 00:00:00 2001 From: diegotry <33488603+diegotry@users.noreply.github.com> Date: Tue, 23 Nov 2021 09:40:31 -0800 Subject: [PATCH 13/35] Update packages/@aws-cdk/aws-apigateway/README.md Co-authored-by: Niranjan Jayakar --- packages/@aws-cdk/aws-apigateway/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index faf99adfd7393..1c855833cb16b 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -149,7 +149,7 @@ When the REST API endpoint configuration above is invoked using POST, as follows curl -X POST -d '{ "customerId": 1 }' https://example.com/ ``` -AWS Step Functions will receive the request body inside a `body` key as it is possible to have other keys within the request (ex. requestContext, header, path, and/or querystring): +AWS Step Functions will receive the request body in its input as follows: ```json { From 16ef95130b50b8de606e9b0bdcda7a86690fe202 Mon Sep 17 00:00:00 2001 From: Diego Santiviago Date: Tue, 23 Nov 2021 12:43:24 -0800 Subject: [PATCH 14/35] Update comments/docs to fix Rosetta --- packages/@aws-cdk/aws-stepfunctions/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions/README.md b/packages/@aws-cdk/aws-stepfunctions/README.md index b57f73ac7562a..04eface3844a0 100644 --- a/packages/@aws-cdk/aws-stepfunctions/README.md +++ b/packages/@aws-cdk/aws-stepfunctions/README.md @@ -623,8 +623,8 @@ import * as logs from '@aws-cdk/aws-logs'; const logGroup = new logs.LogGroup(this, 'MyLogGroup'); -new stepfunctions.StateMachine(this, 'MyStateMachine', { - definition: stepfunctions.Chain.start(new sfn.Pass(this, 'Pass')), +new sfn.StateMachine(this, 'MyStateMachine', { + definition: sfn.Chain.start(new sfn.Pass(this, 'Pass')), logs: { destination: logGroup, level: sfn.LogLevel.ALL, From d0fe918791881acb74c6db70f73f03a3b0b76b9d Mon Sep 17 00:00:00 2001 From: Diego Santiviago Date: Tue, 23 Nov 2021 13:44:30 -0800 Subject: [PATCH 15/35] Update SFN examples for Rosetta --- packages/@aws-cdk/aws-stepfunctions/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions/README.md b/packages/@aws-cdk/aws-stepfunctions/README.md index 04eface3844a0..e74367df84676 100644 --- a/packages/@aws-cdk/aws-stepfunctions/README.md +++ b/packages/@aws-cdk/aws-stepfunctions/README.md @@ -637,8 +637,8 @@ new sfn.StateMachine(this, 'MyStateMachine', { Enable X-Ray tracing for StateMachine: ```ts -new stepfunctions.StateMachine(this, 'MyStateMachine', { - definition: stepfunctions.Chain.start(new stepfunctions.Pass(this, 'Pass')), +new sfn.StateMachine(this, 'MyStateMachine', { + definition: sfn.Chain.start(new sfn.Pass(this, 'Pass')), tracingEnabled: true, }); ``` From 663ece7d3b014bc129dba5a1c77cc3201ea28245 Mon Sep 17 00:00:00 2001 From: Diego Santiviago Date: Tue, 23 Nov 2021 22:31:31 -0800 Subject: [PATCH 16/35] Refactor requestContext dynamic options and update documentation --- packages/@aws-cdk/aws-apigateway/README.md | 55 ++-- .../lib/integrations/request-context.ts | 45 +-- .../lib/integrations/stepfunctions.ts | 154 ++++------- .../lib/integrations/stepfunctions.vtl | 12 +- .../aws-apigateway/lib/stepfunctions-api.ts | 6 +- ...stepfunctions-api-all-params.expected.json | 261 ------------------ .../integ.stepfunctions-api-all-params.ts | 64 ----- ...ns-api-request-context-empty.expected.json | 261 ------------------ ...stepfunctions-api-request-context-empty.ts | 42 --- ...-context-multiple-properties.expected.json | 261 ------------------ ...api-request-context-multiple-properties.ts | 45 --- ...uest-context-single-property.expected.json | 261 ------------------ ...ons-api-request-context-single-property.ts | 44 --- .../integ.stepfunctions-api.expected.json | 4 +- .../test/integ.stepfunctions-api.ts | 7 + .../test/integrations/stepfunctions.test.ts | 30 +- .../test/stepfunctions-api.test.ts | 12 +- 17 files changed, 162 insertions(+), 1402 deletions(-) delete mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-all-params.expected.json delete mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-all-params.ts delete mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-empty.expected.json delete mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-empty.ts delete mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-multiple-properties.expected.json delete mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-multiple-properties.ts delete mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-single-property.expected.json delete mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-single-property.ts diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 1c855833cb16b..c0bef4bb13f9e 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -115,7 +115,7 @@ The `StepFunctionsRestApi` only supports integration with Synchronous Express st The construct sets up an API endpoint and maps the `ANY` HTTP method and any calls to the API endpoint starts an express workflow execution for the underlying state machine. -Invoking either `GET` or `POST` in the example below will send the request to the state machine as a new execution. On success, an HTTP code `200` is returned with the execution output as the Response Body. +Invoking the endpoint with any HTTP method (`GET`, `POST`, `PUT`, `DELETE`, ...) in the example below will send the request to the state machine as a new execution. On success, an HTTP code `200` is returned with the execution output as the Response Body. If the execution fails, an HTTP `500` response is returned with the `error` and `cause` from the execution output as the Response Body. If the request is invalid (ex. bad execution input) HTTP code `400` is returned. @@ -161,10 +161,10 @@ AWS Step Functions will receive the request body in its input as follows: } ``` -Example 2: GET with path `/users/{userId}`. +When the endpoint is invoked at path '/users/5' using the HTTP GET method as below: ```bash -curl -X GET https://example.com/users/{userId} +curl -X GET https://example.com/users/5?foo=bar ``` AWS Step Functions will receive the following execution input: @@ -172,35 +172,56 @@ AWS Step Functions will receive the following execution input: ```json { "body": {}, - "path": "/users/{userId}", - "querystring": {} + "path": { + "users": "5" + }, + "querystring": { + "foo": "bar" + } } ``` -Example 3: GET with `headers`, `querystring` and `requestContext` enabled. +It is possible to combine included/excluded fields. For example, `headers` and the `requestContext` can be included while `path` and `querystring` are excluded through the following configuration: + +```ts +const stateMachineDefinition = new stepfunctions.Pass(this, 'PassState'); + +const stateMachine: stepfunctions.IStateMachine = new stepfunctions.StateMachine(this, 'StateMachine', { + definition: stateMachineDefinition, + stateMachineType: stepfunctions.StateMachineType.EXPRESS, +}); + +new apigateway.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { + stateMachine: stateMachine, + headers: true, + path: false, + querystring: false, + requestContext: { + caller: true, + user: true, + }, +}); +``` + +When the endpoint is invoked at path '/' using the HTTP GET method as below: ```bash -curl https://example.com/?customerId=1 +curl -X GET https://example.com/ ``` AWS Step Functions will receive the following execution input: -Request: - ```json { - "body": {}, + "headers": { + "Accept": "...", + "CloudFront-Forwarded-Proto": "...", + }, "requestContext": { "accountId": "...", "apiKey": "...", }, - "header": { - "Accept": "...", - "CloudFront-Forwarded-Proto": "...", - }, - "querystring": { - "customerId": "1" - } + "body": {} } ``` diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/request-context.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/request-context.ts index 33065bba2a20d..ee7e7fa617505 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/request-context.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/request-context.ts @@ -9,7 +9,7 @@ export interface RequestContext { /** * Represents the information of $context.identity.accountId * - * The AWS account ID associated with the request. + * Whether the AWS account of the API owner should be included in the request context * @default false */ readonly accountId?: boolean; @@ -17,7 +17,7 @@ export interface RequestContext { /** * Represents the information of $context.apiId * - * The identifier API Gateway assigns to your API. + * Whether the identifier API Gateway assigns to your API should be included in the request context. * @default false */ readonly apiId?: boolean; @@ -25,8 +25,7 @@ export interface RequestContext { /** * Represents the information of $context.identity.apiKey * - * For API methods that require an API key, this variable is the API key associated with the method request. - * For methods that don't require an API key, this variable is null. + * Whether the API key associated with the request should be included in request context. * @default false */ readonly apiKey?: boolean; @@ -34,7 +33,8 @@ export interface RequestContext { /** * Represents the information of $context.authorizer.principalId * - * The principal user identification associated with the token sent by the client and returned from an API Gateway Lambda authorizer (formerly known as a custom authorizer) + * Whether the principal user identifier associated with the token sent by the client and returned + * from an API Gateway Lambda authorizer should be included in the request context. * @default false */ readonly authorizerPrincipalId?: boolean; @@ -42,7 +42,8 @@ export interface RequestContext { /** * Represents the information of $context.identity.caller * - * The principal identifier of the caller that signed the request. Supported for resources that use IAM authorization. + * Whether the principal identifier of the caller that signed the request should be included in the request context. + * Supported for resources that use IAM authorization. * @default false */ readonly caller?: boolean; @@ -50,7 +51,8 @@ export interface RequestContext { /** * Represents the information of $context.identity.cognitoAuthenticationProvider * - * A comma-separated list of the Amazon Cognito authentication providers used by the caller making the request. Available only if the request was signed with Amazon Cognito credentials. + * Whether the list of the Amazon Cognito authentication providers used by the caller making the request should be included in the request context. + * Available only if the request was signed with Amazon Cognito credentials. * @default false */ readonly cognitoAuthenticationProvider?: boolean; @@ -58,7 +60,7 @@ export interface RequestContext { /** * Represents the information of $context.identity.cognitoAuthenticationType * - * The Amazon Cognito authentication type of the caller making the request. + * Whether the Amazon Cognito authentication type of the caller making the request should be included in the request context. * Available only if the request was signed with Amazon Cognito credentials. * Possible values include authenticated for authenticated identities and unauthenticated for unauthenticated identities. * @default false @@ -68,7 +70,8 @@ export interface RequestContext { /** * Represents the information of $context.identity.cognitoIdentityId * - * The Amazon Cognito identity ID of the caller making the request. Available only if the request was signed with Amazon Cognito credentials. + * Whether the Amazon Cognito identity ID of the caller making the request should be included in the request context. + * Available only if the request was signed with Amazon Cognito credentials. * @default false */ readonly cognitoIdentityId?: boolean; @@ -76,7 +79,8 @@ export interface RequestContext { /** * Represents the information of $context.identity.cognitoIdentityPoolId * - * The Amazon Cognito identity pool ID of the caller making the request. Available only if the request was signed with Amazon Cognito credentials. + * Whether the Amazon Cognito identity pool ID of the caller making the request should be included in the request context. + * Available only if the request was signed with Amazon Cognito credentials. * @default false */ readonly cognitoIdentityPoolId?: boolean; @@ -84,7 +88,8 @@ export interface RequestContext { /** * Represents the information of $context.httpMethod * - * The HTTP method used. Valid values include: DELETE, GET, HEAD, OPTIONS, PATCH, POST, and PUT. + * Whether the HTTP method used should be included in the request context. + * Valid values include: DELETE, GET, HEAD, OPTIONS, PATCH, POST, and PUT. * @default false */ readonly httpMethod?: boolean; @@ -92,7 +97,7 @@ export interface RequestContext { /** * Represents the information of $context.stage * - * The deployment stage of the API request (for example, Beta or Prod). + * Whether the deployment stage of the API request should be included in the request context. * @default false */ readonly stage?: boolean; @@ -100,7 +105,8 @@ export interface RequestContext { /** * Represents the information of $context.identity.sourceIp * - * The source IP address of the immediate TCP connection making the request to API Gateway endpoint. + * Whether the source IP address of the immediate TCP connection making the request + * to API Gateway endpoint should be included in the request context. * @default false */ readonly sourceIp?: boolean; @@ -108,7 +114,8 @@ export interface RequestContext { /** * Represents the information of $context.identity.user * - * The principal identifier of the user that will be authorized against resource access. Supported for resources that use IAM authorization. + * Whether the principal identifier of the user that will be authorized should be included in the request context. + * Supported for resources that use IAM authorization. * @default false */ readonly user?: boolean; @@ -116,7 +123,7 @@ export interface RequestContext { /** * Represents the information of $context.identity.userAgent * - * The User-Agent header of the API caller. + * Whether the User-Agent header of the API caller should be included in the request context. * @default false */ readonly userAgent?: boolean; @@ -124,7 +131,7 @@ export interface RequestContext { /** * Represents the information of $context.identity.userArn * - * The Amazon Resource Name (ARN) of the effective user identified after authentication. + * Whether the Amazon Resource Name (ARN) of the effective user identified after authentication should be included in the request context. * @default false */ readonly userArn?: boolean; @@ -132,7 +139,7 @@ export interface RequestContext { /** * Represents the information of $context.requestId * - * An ID for the request. Clients can override this request ID. + * Whether the ID for the request should be included in the request context. * @default false */ readonly requestId?: boolean; @@ -140,7 +147,7 @@ export interface RequestContext { /** * Represents the information of $context.resourceId * - * The identifier that API Gateway assigns to your resource. + * Whether the identifier that API Gateway assigns to your resource should be included in the request context. * @default false */ readonly resourceId?: boolean; @@ -148,7 +155,7 @@ export interface RequestContext { /** * Represents the information of $context.resourcePath * - * The path to your resource. + * Whether the path to the resource should be included in the request context. * @default false */ readonly resourcePath?: boolean; diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts index a9adaf7af80c1..e0d89ab94647a 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts @@ -10,7 +10,7 @@ import { AwsIntegration } from './aws'; /** * Options when configuring Step Functions synchronous integration with Rest API */ -export interface StepFunctionsSynchronousIntegrationOptions extends IntegrationOptions { +export interface StepFunctionsExecutionIntegrationOptions extends IntegrationOptions { /** * Which details of the incoming request must be passed onto the underlying state machine, @@ -79,11 +79,11 @@ export interface StepFunctionsSynchronousIntegrationOptions extends IntegrationO * const api = new apigateway.RestApi(this, 'Api', { * restApiName: 'MyApi', * }); - * api.root.addMethod('GET', new apigateway.StepFunctionsSynchronousIntegration(stateMachine)); + * api.root.addMethod('GET', new apigateway.StepFunctionsExecutionIntegration(stateMachine)); */ -export class StepFunctionsSynchronousIntegration extends AwsIntegration { +export class StepFunctionsExecutionIntegration extends AwsIntegration { private readonly stateMachine: sfn.IStateMachine; - constructor(stateMachine: sfn.IStateMachine, options: StepFunctionsSynchronousIntegrationOptions = {}) { + constructor(stateMachine: sfn.IStateMachine, options: StepFunctionsExecutionIntegrationOptions = {}) { super({ service: 'states', action: 'StartSyncExecution', @@ -108,6 +108,11 @@ export class StepFunctionsSynchronousIntegration extends AwsIntegration { let stateMachineName; if (this.stateMachine instanceof sfn.StateMachine) { + const stateMachineType = (this.stateMachine as sfn.StateMachine).stateMachineType; + if (stateMachineType !== sfn.StateMachineType.EXPRESS) { + throw new Error('State Machine must be of type "EXPRESS". Please use StateMachineType.EXPRESS as the stateMachineType'); + } + //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.stateMachine.node.defaultChild as sfn.CfnStateMachine).stateMachineName; @@ -166,17 +171,19 @@ function integrationResponse() { { statusCode: '200', responseTemplates: { + /* eslint-disable */ '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\')"', - '}', + '#if($input.path(\'$.status\').toString().equals("FAILED"))', + '#set($context.responseOverride.status = 500)', + '{', + '"error": "$input.path(\'$.error\')"', + '"cause": "$input.path(\'$.cause\')"', + '}', '#else', - '$input.path(\'$.output\')', + '$input.path(\'$.output\')', '#end', + /* eslint-enable */ ].join('\n'), }, }, @@ -186,41 +193,14 @@ function integrationResponse() { return integResponse; } -/** - * Checks each property of the RequestContext to see if request context has - * a property specified and then return true or false. - * @param requestContextObj - * @returns boolean - */ -function checkIncludeRequestContext(requestContextObj: RequestContext | undefined) { - - if (requestContextObj) { - if (!requestContextObj.accountId && !requestContextObj.apiId && !requestContextObj.apiKey && - !requestContextObj.authorizerPrincipalId && !requestContextObj.caller && - !requestContextObj.cognitoAuthenticationProvider && !requestContextObj.cognitoAuthenticationType && - !requestContextObj.cognitoIdentityId && !requestContextObj.cognitoIdentityPoolId && - !requestContextObj.httpMethod && !requestContextObj.stage && !requestContextObj.sourceIp && - !requestContextObj.user && !requestContextObj.userAgent && - !requestContextObj.userArn && !requestContextObj.requestId && - !requestContextObj.resourceId && !requestContextObj.resourcePath) { - return false; - } else { - return true; - } - } else { - return false; - } -} - /** * Defines the request template that will be used for the integration * @param stateMachine * @param options * @returns requestTemplate */ -function requestTemplates(stateMachine: sfn.IStateMachine, options: StepFunctionsSynchronousIntegrationOptions) { - let includeRequestContext: boolean = checkIncludeRequestContext(options.requestContext); - const templateStr = templateString(stateMachine, includeRequestContext, options); +function requestTemplates(stateMachine: sfn.IStateMachine, options: StepFunctionsExecutionIntegrationOptions) { + const templateStr = templateString(stateMachine, options); const requestTemplate: { [contentType:string] : string } = { @@ -241,8 +221,7 @@ function requestTemplates(stateMachine: sfn.IStateMachine, options: StepFunction */ function templateString( stateMachine: sfn.IStateMachine, - includeRequestContext: boolean, - options: StepFunctionsSynchronousIntegrationOptions): string { + options: StepFunctionsExecutionIntegrationOptions): string { let templateStr: string; let requestContextStr = ''; @@ -251,7 +230,7 @@ function templateString( const includeQueryString = options.querystring?? true; const includePath = options.path?? true; - if (includeRequestContext) { + if (options.requestContext && Object.keys(options.requestContext).length > 0) { requestContextStr = requestContext(options.requestContext); } @@ -265,70 +244,33 @@ function templateString( return templateStr; } -/** - * Builder function that builds the request context string - * for when request context is asked for the execution input. - * - * @param requestContextInterface - * @returns reqeustContextStr - */ -function requestContextBuilder(requestContextInterface:RequestContext): string { - const contextStr: string = '"requestContext": {'; - const accountIdStr: string = (requestContextInterface.accountId) ? '"accountId":"$context.identity.accountId",' : ''; - const apiIdStr: string = (requestContextInterface.apiId) ? '"apiId":"$context.apiId",' : ''; - const apiKeyStr: string = (requestContextInterface.apiKey) ? '"apiKey":"$context.identity.apiKey",' : ''; - const authorizerPrincipalIdStr: string = (requestContextInterface.authorizerPrincipalId) ? '"authorizerPrincipalId":"$context.authorizer.principalId",' : ''; - const callerStr: string = (requestContextInterface.caller) ? '"caller":"$context.identity.caller",' : ''; - const cognitoAuthenticationProviderStr: string = (requestContextInterface.cognitoAuthenticationProvider) ? '"cognitoAuthenticationProvider":"$context.identity.cognitoAuthenticationProvider",' : ''; - const cognitoAuthenticationTypeStr: string = (requestContextInterface.cognitoAuthenticationType) ? '"cognitoAuthenticationType":"$context.identity.cognitoAuthenticationType",' : ''; - const cognitoIdentityIdStr: string = (requestContextInterface.cognitoIdentityId) ? '"cognitoIdentityId":"$context.identity.cognitoIdentityId",' : ''; - const cognitoIdentityPoolIdStr: string = (requestContextInterface.cognitoIdentityPoolId) ? '"cognitoIdentityPoolId":"$context.identity.cognitoIdentityPoolId",' : ''; - const httpMethodStr: string = (requestContextInterface.httpMethod) ? '"httpMethod":"$context.httpMethod",' : ''; - const stageStr: string = (requestContextInterface.stage) ? '"stage":"$context.stage",' : ''; - const sourceIpStr: string = (requestContextInterface.sourceIp) ? '"sourceIp":"$context.identity.sourceIp",' : ''; - const userStr: string = (requestContextInterface.user) ? '"user":"$context.identity.user",' : ''; - const userAgentStr: string = (requestContextInterface.userAgent) ? '"userAgent":"$context.identity.userAgent",' : ''; - const userArnStr: string = (requestContextInterface.userArn) ? '"userArn":"$context.identity.userArn",' : ''; - const requestIdStr: string = (requestContextInterface.requestId) ? '"requestId":"$context.requestId",' : ''; - const resourceIdStr: string = (requestContextInterface.resourceId) ? '"resourceId":"$context.resourceId",' : ''; - const resourcePathStr: string = (requestContextInterface.resourcePath) ? '"resourcePath":"$context.resourcePath",' : ''; - const endStr = '}'; - - let requestContextString :string = contextStr + accountIdStr + apiIdStr + apiKeyStr + - authorizerPrincipalIdStr + callerStr + cognitoAuthenticationProviderStr + cognitoAuthenticationTypeStr + - cognitoIdentityIdStr + cognitoIdentityPoolIdStr + httpMethodStr + stageStr + sourceIpStr + userStr + - userAgentStr + userArnStr + requestIdStr + resourceIdStr + resourcePathStr + endStr; - - if (requestContextString !== (contextStr + endStr)) { - //Removing the last comma froom the string only if it has a value inside - requestContextString = requestContextString.substring(0, requestContextString.length-2) + '}'; - } - return requestContextString; -} - function requestContext(requestContextObj: RequestContext | undefined): string { - const requestContextStr: string = requestContextBuilder({ - accountId: (requestContextObj) ? requestContextObj?.accountId : false, - apiId: (requestContextObj) ? requestContextObj?.apiId : false, - apiKey: (requestContextObj) ? requestContextObj?.apiKey : false, - authorizerPrincipalId: (requestContextObj) ? requestContextObj?.authorizerPrincipalId : false, - caller: (requestContextObj) ? requestContextObj?.caller : false, - cognitoAuthenticationProvider: (requestContextObj) ? requestContextObj?.cognitoAuthenticationProvider : false, - cognitoAuthenticationType: (requestContextObj) ? requestContextObj?.cognitoAuthenticationType : false, - cognitoIdentityId: (requestContextObj) ? requestContextObj?.cognitoIdentityId : false, - cognitoIdentityPoolId: (requestContextObj) ? requestContextObj?.cognitoIdentityPoolId : false, - httpMethod: (requestContextObj) ? requestContextObj?.httpMethod : false, - stage: (requestContextObj) ? requestContextObj?.stage : false, - sourceIp: (requestContextObj) ? requestContextObj?.sourceIp : false, - user: (requestContextObj) ? requestContextObj?.user : false, - userAgent: (requestContextObj) ? requestContextObj?.userAgent : false, - userArn: (requestContextObj) ? requestContextObj?.userArn : false, - requestId: (requestContextObj) ? requestContextObj?.requestId : false, - resourceId: (requestContextObj) ? requestContextObj?.resourceId : false, - resourcePath: (requestContextObj) ? requestContextObj?.resourcePath : false, - }); + const context = { + accountId: requestContextObj?.accountId? '$context.identity.accountId': undefined, + apiId: requestContextObj?.apiId? '$context.apiId': undefined, + apiKey: requestContextObj?.apiKey? '$context.identity.apiKey': undefined, + authorizerPrincipalId: requestContextObj?.authorizerPrincipalId? '$context.authorizer.principalId': undefined, + caller: requestContextObj?.caller? '$context.identity.caller': undefined, + cognitoAuthenticationProvider: requestContextObj?.cognitoAuthenticationProvider? '$context.identity.cognitoAuthenticationProvider': undefined, + cognitoAuthenticationType: requestContextObj?.cognitoAuthenticationType? '$context.identity.cognitoAuthenticationType': undefined, + cognitoIdentityId: requestContextObj?.cognitoIdentityId? '$context.identity.cognitoIdentityId': undefined, + cognitoIdentityPoolId: requestContextObj?.cognitoIdentityPoolId? '$context.identity.cognitoIdentityPoolId': undefined, + httpMethod: requestContextObj?.httpMethod? '$context.httpMethod': undefined, + stage: requestContextObj?.stage? '$context.stage': undefined, + sourceIp: requestContextObj?.sourceIp? '$context.identity.sourceIp': undefined, + user: requestContextObj?.user? '$context.identity.user': undefined, + userAgent: requestContextObj?.userAgent? '$context.identity.userAgent': undefined, + userArn: requestContextObj?.userArn? '$context.identity.userArn': undefined, + requestId: requestContextObj?.requestId? '$context.requestId': undefined, + resourceId: requestContextObj?.resourceId? '$context.resourceId': undefined, + resourcePath: requestContextObj?.resourcePath? '$context.resourcePath': undefined, + }; + + const contextAsString = JSON.stringify(context); - const search = '"'; + // The VTL Template conflicts with double-quotes (") for strings. + // Before sending to the template, we replace double-quotes (") with @@ and replace it back inside the .vtl file + const doublequotes = '"'; const replaceWith = '@@'; - return requestContextStr.split(search).join(replaceWith); + return contextAsString.split(doublequotes).join(replaceWith); } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.vtl b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.vtl index e59c61aa93fff..df4eda7c279d5 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.vtl +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.vtl @@ -28,7 +28,6 @@ #end #if ($includeQueryString) - #set($inputString = "$inputString, @@querystring@@:{") #foreach($paramName in $allParams.querystring.keySet()) #set($inputString = "$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@") @@ -40,14 +39,21 @@ #end #if ($includePath) - #set($inputString = "$inputString, @@path@@: @@$context.resourcePath@@") + #set($inputString = "$inputString, @@path@@:{") + #foreach($paramName in $allParams.path.keySet()) + #set($inputString = "$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.path.get($paramName))@@") + #if($foreach.hasNext) + #set($inputString = "$inputString,") + #end + #end + #set($inputString = "$inputString }") #end #set($requestContext = "%REQUESTCONTEXT%") ## Check if the request context should be included as part of the execution input #if($requestContext && !$requestContext.empty) #set($inputString = "$inputString,") - #set($inputString = "$inputString $requestContext") + #set($inputString = "$inputString @@requestContext@@: $requestContext") #end #set($inputString = "$inputString}") diff --git a/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts b/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts index 24783e7e876a8..f032f1e95cf92 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts @@ -3,11 +3,11 @@ import * as sfn from '@aws-cdk/aws-stepfunctions'; import { Construct } from 'constructs'; import { RestApi, RestApiProps } from '.'; import { RequestContext } from './integrations'; -import { StepFunctionsSynchronousIntegration } from './integrations/stepfunctions'; +import { StepFunctionsExecutionIntegration } from './integrations/stepfunctions'; import { Model } from './model'; /** - * Propeties for StepFunctionsRestApi + * Properties for StepFunctionsRestApi * */ export interface StepFunctionsRestApiProps extends RestApiProps { @@ -63,7 +63,7 @@ export class StepFunctionsRestApi extends RestApi { throw new Error('State Machine must be of type "EXPRESS". Please use StateMachineType.EXPRESS as the stateMachineType'); } - const stepfunctionsIntegration = new StepFunctionsSynchronousIntegration(props.stateMachine, { + const stepfunctionsIntegration = new StepFunctionsExecutionIntegration(props.stateMachine, { credentialsRole: role(scope, props), requestContext: props.requestContext, path: props.path?? true, diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-all-params.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-all-params.expected.json deleted file mode 100644 index a19dd993f108a..0000000000000 --- a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-all-params.expected.json +++ /dev/null @@ -1,261 +0,0 @@ -{ - "Resources": { - "StateMachineRoleB840431D": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": { - "Fn::Join": [ - "", - [ - "states.", - { - "Ref": "AWS::Region" - }, - ".amazonaws.com" - ] - ] - } - } - } - ], - "Version": "2012-10-17" - } - } - }, - "StateMachine2E01A3A5": { - "Type": "AWS::StepFunctions::StateMachine", - "Properties": { - "RoleArn": { - "Fn::GetAtt": [ - "StateMachineRoleB840431D", - "Arn" - ] - }, - "DefinitionString": "{\"StartAt\":\"PassTask\",\"States\":{\"PassTask\":{\"Type\":\"Pass\",\"Result\":\"Hello\",\"End\":true}}}", - "StateMachineType": "EXPRESS" - }, - "DependsOn": [ - "StateMachineRoleB840431D" - ] - }, - "StartSyncExecutionRoleDE73CB90": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "apigateway.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - } - } - }, - "StartSyncExecutionRoleDefaultPolicy5A5803F8": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": "states:StartSyncExecution", - "Effect": "Allow", - "Resource": { - "Ref": "StateMachine2E01A3A5" - } - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "StartSyncExecutionRoleDefaultPolicy5A5803F8", - "Roles": [ - { - "Ref": "StartSyncExecutionRoleDE73CB90" - } - ] - } - }, - "StepFunctionsRestApiC6E3E883": { - "Type": "AWS::ApiGateway::RestApi", - "Properties": { - "Name": "StepFunctionsRestApi" - } - }, - "StepFunctionsRestApiCloudWatchRoleB06ACDB9": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "apigateway.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "ManagedPolicyArns": [ - { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" - ] - ] - } - ] - } - }, - "StepFunctionsRestApiAccountBD0CCC0E": { - "Type": "AWS::ApiGateway::Account", - "Properties": { - "CloudWatchRoleArn": { - "Fn::GetAtt": [ - "StepFunctionsRestApiCloudWatchRoleB06ACDB9", - "Arn" - ] - } - }, - "DependsOn": [ - "StepFunctionsRestApiC6E3E883" - ] - }, - "StepFunctionsRestApiANY7699CA92": { - "Type": "AWS::ApiGateway::Method", - "Properties": { - "HttpMethod": "ANY", - "ResourceId": { - "Fn::GetAtt": [ - "StepFunctionsRestApiC6E3E883", - "RootResourceId" - ] - }, - "RestApiId": { - "Ref": "StepFunctionsRestApiC6E3E883" - }, - "AuthorizationType": "NONE", - "Integration": { - "Credentials": { - "Fn::GetAtt": [ - "StartSyncExecutionRoleDE73CB90", - "Arn" - ] - }, - "IntegrationHttpMethod": "POST", - "IntegrationResponses": [ - { - "ResponseTemplates": { - "application/json": "#set($inputRoot = $input.path('$'))\n#if($input.path('$.status').toString().equals(\"FAILED\"))\n#set($context.responseOverride.status = 500)\n{\n\"error\": \"$input.path('$.error')\"\n\"cause\": \"$input.path('$.cause')\"\n}\n#else\n$input.path('$.output')\n#end" - }, - "StatusCode": "200" - }, - { - "ResponseTemplates": { - "application/json": "{\n \"error\": \"Bad request!\"\n }" - }, - "SelectionPattern": "4\\d{2}", - "StatusCode": "400" - }, - { - "ResponseTemplates": { - "application/json": "\"error\": $input.path('$.error')" - }, - "SelectionPattern": "5\\d{2}", - "StatusCode": "500" - } - ], - "PassthroughBehavior": "NEVER", - "RequestTemplates": { - "application/json": { - "Fn::Join": [ - "", - [ - "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = true)\n#set($includeQueryString = true)\n#set($includePath = true)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", - { - "Ref": "StateMachine2E01A3A5" - }, - "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n \n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@: @@$context.resourcePath@@\")\n #end\n \n #set($requestContext = \"@@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@@}\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n" - ] - ] - } - }, - "Type": "AWS", - "Uri": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":apigateway:", - { - "Ref": "AWS::Region" - }, - ":states:action/StartSyncExecution" - ] - ] - } - }, - "MethodResponses": [ - { - "ResponseModels": { - "application/json": "Empty" - }, - "StatusCode": "200" - }, - { - "ResponseModels": { - "application/json": "Error" - }, - "StatusCode": "400" - }, - { - "ResponseModels": { - "application/json": "Error" - }, - "StatusCode": "500" - } - ] - } - }, - "deployment33381975b5dafda9a97138f301ea25da405640e8": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "StepFunctionsRestApiC6E3E883" - } - }, - "DependsOn": [ - "StepFunctionsRestApiANY7699CA92" - ] - }, - "stage0661E4AC": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "StepFunctionsRestApiC6E3E883" - }, - "DeploymentId": { - "Ref": "deployment33381975b5dafda9a97138f301ea25da405640e8" - }, - "StageName": "prod" - } - } - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-all-params.ts b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-all-params.ts deleted file mode 100644 index 215db9f7f053b..0000000000000 --- a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-all-params.ts +++ /dev/null @@ -1,64 +0,0 @@ -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as cdk from '@aws-cdk/core'; -import { Construct } from 'constructs'; -import * as apigw from '../lib'; - -/** - * Stack verification steps: - * * `curl -X POST 'https://.execute-api..amazonaws.com/prod' \ - * * -d '{"key":"Hello"}' -H 'Content-Type: application/json'` - * The above should return a "Hello" response - */ - -class StepFunctionsRestApiDeploymentStack extends cdk.Stack { - constructor(scope: Construct) { - super(scope, 'StepFunctionsRestApiDeploymentStack'); - - const passTask = new sfn.Pass(this, 'PassTask', { - result: { value: 'Hello' }, - }); - - const stateMachine = new sfn.StateMachine(this, 'StateMachine', { - definition: passTask, - stateMachineType: sfn.StateMachineType.EXPRESS, - }); - - const api = new apigw.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { - deploy: false, - stateMachine: stateMachine, - headers: true, - querystring: true, - path: true, - requestContext: { - accountId: true, - apiId: true, - apiKey: true, - authorizerPrincipalId: true, - caller: true, - cognitoAuthenticationProvider: true, - cognitoAuthenticationType: true, - cognitoIdentityId: true, - cognitoIdentityPoolId: true, - httpMethod: true, - stage: true, - sourceIp: true, - user: true, - userAgent: true, - userArn: true, - requestId: true, - resourceId: true, - resourcePath: true, - }, - }); - - api.deploymentStage = new apigw.Stage(this, 'stage', { - deployment: new apigw.Deployment(this, 'deployment', { - api, - }), - }); - } -} - -const app = new cdk.App(); -new StepFunctionsRestApiDeploymentStack(app); -app.synth(); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-empty.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-empty.expected.json deleted file mode 100644 index 51d4c989e9564..0000000000000 --- a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-empty.expected.json +++ /dev/null @@ -1,261 +0,0 @@ -{ - "Resources": { - "StateMachineRoleB840431D": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": { - "Fn::Join": [ - "", - [ - "states.", - { - "Ref": "AWS::Region" - }, - ".amazonaws.com" - ] - ] - } - } - } - ], - "Version": "2012-10-17" - } - } - }, - "StateMachine2E01A3A5": { - "Type": "AWS::StepFunctions::StateMachine", - "Properties": { - "RoleArn": { - "Fn::GetAtt": [ - "StateMachineRoleB840431D", - "Arn" - ] - }, - "DefinitionString": "{\"StartAt\":\"PassTask\",\"States\":{\"PassTask\":{\"Type\":\"Pass\",\"Result\":\"Hello\",\"End\":true}}}", - "StateMachineType": "EXPRESS" - }, - "DependsOn": [ - "StateMachineRoleB840431D" - ] - }, - "StartSyncExecutionRoleDE73CB90": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "apigateway.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - } - } - }, - "StartSyncExecutionRoleDefaultPolicy5A5803F8": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": "states:StartSyncExecution", - "Effect": "Allow", - "Resource": { - "Ref": "StateMachine2E01A3A5" - } - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "StartSyncExecutionRoleDefaultPolicy5A5803F8", - "Roles": [ - { - "Ref": "StartSyncExecutionRoleDE73CB90" - } - ] - } - }, - "StepFunctionsRestApiC6E3E883": { - "Type": "AWS::ApiGateway::RestApi", - "Properties": { - "Name": "StepFunctionsRestApi" - } - }, - "StepFunctionsRestApiCloudWatchRoleB06ACDB9": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "apigateway.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "ManagedPolicyArns": [ - { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" - ] - ] - } - ] - } - }, - "StepFunctionsRestApiAccountBD0CCC0E": { - "Type": "AWS::ApiGateway::Account", - "Properties": { - "CloudWatchRoleArn": { - "Fn::GetAtt": [ - "StepFunctionsRestApiCloudWatchRoleB06ACDB9", - "Arn" - ] - } - }, - "DependsOn": [ - "StepFunctionsRestApiC6E3E883" - ] - }, - "StepFunctionsRestApiANY7699CA92": { - "Type": "AWS::ApiGateway::Method", - "Properties": { - "HttpMethod": "ANY", - "ResourceId": { - "Fn::GetAtt": [ - "StepFunctionsRestApiC6E3E883", - "RootResourceId" - ] - }, - "RestApiId": { - "Ref": "StepFunctionsRestApiC6E3E883" - }, - "AuthorizationType": "NONE", - "Integration": { - "Credentials": { - "Fn::GetAtt": [ - "StartSyncExecutionRoleDE73CB90", - "Arn" - ] - }, - "IntegrationHttpMethod": "POST", - "IntegrationResponses": [ - { - "ResponseTemplates": { - "application/json": "#set($inputRoot = $input.path('$'))\n#if($input.path('$.status').toString().equals(\"FAILED\"))\n#set($context.responseOverride.status = 500)\n{\n\"error\": \"$input.path('$.error')\"\n\"cause\": \"$input.path('$.cause')\"\n}\n#else\n$input.path('$.output')\n#end" - }, - "StatusCode": "200" - }, - { - "ResponseTemplates": { - "application/json": "{\n \"error\": \"Bad request!\"\n }" - }, - "SelectionPattern": "4\\d{2}", - "StatusCode": "400" - }, - { - "ResponseTemplates": { - "application/json": "\"error\": $input.path('$.error')" - }, - "SelectionPattern": "5\\d{2}", - "StatusCode": "500" - } - ], - "PassthroughBehavior": "NEVER", - "RequestTemplates": { - "application/json": { - "Fn::Join": [ - "", - [ - "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = true)\n#set($includePath = true)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", - { - "Ref": "StateMachine2E01A3A5" - }, - "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n \n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@: @@$context.resourcePath@@\")\n #end\n \n #set($requestContext = \"\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n" - ] - ] - } - }, - "Type": "AWS", - "Uri": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":apigateway:", - { - "Ref": "AWS::Region" - }, - ":states:action/StartSyncExecution" - ] - ] - } - }, - "MethodResponses": [ - { - "ResponseModels": { - "application/json": "Empty" - }, - "StatusCode": "200" - }, - { - "ResponseModels": { - "application/json": "Error" - }, - "StatusCode": "400" - }, - { - "ResponseModels": { - "application/json": "Error" - }, - "StatusCode": "500" - } - ] - } - }, - "deployment33381975b5dafda9a97138f301ea25da405640e8": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "StepFunctionsRestApiC6E3E883" - } - }, - "DependsOn": [ - "StepFunctionsRestApiANY7699CA92" - ] - }, - "stage0661E4AC": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "StepFunctionsRestApiC6E3E883" - }, - "DeploymentId": { - "Ref": "deployment33381975b5dafda9a97138f301ea25da405640e8" - }, - "StageName": "prod" - } - } - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-empty.ts b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-empty.ts deleted file mode 100644 index 53ba702eee948..0000000000000 --- a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-empty.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as cdk from '@aws-cdk/core'; -import { Construct } from 'constructs'; -import * as apigw from '../lib'; - -/** - * Stack verification steps: - * * `curl -X POST 'https://.execute-api..amazonaws.com/prod' \ - * * -d '{"key":"Hello"}' -H 'Content-Type: application/json'` - * The above should return a "Hello" response - */ - -class StepFunctionsRestApiDeploymentStack extends cdk.Stack { - constructor(scope: Construct) { - super(scope, 'StepFunctionsRestApiDeploymentStack'); - - const passTask = new sfn.Pass(this, 'PassTask', { - result: { value: 'Hello' }, - }); - - const stateMachine = new sfn.StateMachine(this, 'StateMachine', { - definition: passTask, - stateMachineType: sfn.StateMachineType.EXPRESS, - }); - - const api = new apigw.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { - deploy: false, - stateMachine: stateMachine, - requestContext: {}, - }); - - api.deploymentStage = new apigw.Stage(this, 'stage', { - deployment: new apigw.Deployment(this, 'deployment', { - api, - }), - }); - } -} - -const app = new cdk.App(); -new StepFunctionsRestApiDeploymentStack(app); -app.synth(); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-multiple-properties.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-multiple-properties.expected.json deleted file mode 100644 index 205c382d7198e..0000000000000 --- a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-multiple-properties.expected.json +++ /dev/null @@ -1,261 +0,0 @@ -{ - "Resources": { - "StateMachineRoleB840431D": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": { - "Fn::Join": [ - "", - [ - "states.", - { - "Ref": "AWS::Region" - }, - ".amazonaws.com" - ] - ] - } - } - } - ], - "Version": "2012-10-17" - } - } - }, - "StateMachine2E01A3A5": { - "Type": "AWS::StepFunctions::StateMachine", - "Properties": { - "RoleArn": { - "Fn::GetAtt": [ - "StateMachineRoleB840431D", - "Arn" - ] - }, - "DefinitionString": "{\"StartAt\":\"PassTask\",\"States\":{\"PassTask\":{\"Type\":\"Pass\",\"Result\":\"Hello\",\"End\":true}}}", - "StateMachineType": "EXPRESS" - }, - "DependsOn": [ - "StateMachineRoleB840431D" - ] - }, - "StartSyncExecutionRoleDE73CB90": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "apigateway.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - } - } - }, - "StartSyncExecutionRoleDefaultPolicy5A5803F8": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": "states:StartSyncExecution", - "Effect": "Allow", - "Resource": { - "Ref": "StateMachine2E01A3A5" - } - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "StartSyncExecutionRoleDefaultPolicy5A5803F8", - "Roles": [ - { - "Ref": "StartSyncExecutionRoleDE73CB90" - } - ] - } - }, - "StepFunctionsRestApiC6E3E883": { - "Type": "AWS::ApiGateway::RestApi", - "Properties": { - "Name": "StepFunctionsRestApi" - } - }, - "StepFunctionsRestApiCloudWatchRoleB06ACDB9": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "apigateway.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "ManagedPolicyArns": [ - { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" - ] - ] - } - ] - } - }, - "StepFunctionsRestApiAccountBD0CCC0E": { - "Type": "AWS::ApiGateway::Account", - "Properties": { - "CloudWatchRoleArn": { - "Fn::GetAtt": [ - "StepFunctionsRestApiCloudWatchRoleB06ACDB9", - "Arn" - ] - } - }, - "DependsOn": [ - "StepFunctionsRestApiC6E3E883" - ] - }, - "StepFunctionsRestApiANY7699CA92": { - "Type": "AWS::ApiGateway::Method", - "Properties": { - "HttpMethod": "ANY", - "ResourceId": { - "Fn::GetAtt": [ - "StepFunctionsRestApiC6E3E883", - "RootResourceId" - ] - }, - "RestApiId": { - "Ref": "StepFunctionsRestApiC6E3E883" - }, - "AuthorizationType": "NONE", - "Integration": { - "Credentials": { - "Fn::GetAtt": [ - "StartSyncExecutionRoleDE73CB90", - "Arn" - ] - }, - "IntegrationHttpMethod": "POST", - "IntegrationResponses": [ - { - "ResponseTemplates": { - "application/json": "#set($inputRoot = $input.path('$'))\n#if($input.path('$.status').toString().equals(\"FAILED\"))\n#set($context.responseOverride.status = 500)\n{\n\"error\": \"$input.path('$.error')\"\n\"cause\": \"$input.path('$.cause')\"\n}\n#else\n$input.path('$.output')\n#end" - }, - "StatusCode": "200" - }, - { - "ResponseTemplates": { - "application/json": "{\n \"error\": \"Bad request!\"\n }" - }, - "SelectionPattern": "4\\d{2}", - "StatusCode": "400" - }, - { - "ResponseTemplates": { - "application/json": "\"error\": $input.path('$.error')" - }, - "SelectionPattern": "5\\d{2}", - "StatusCode": "500" - } - ], - "PassthroughBehavior": "NEVER", - "RequestTemplates": { - "application/json": { - "Fn::Join": [ - "", - [ - "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = true)\n#set($includePath = true)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", - { - "Ref": "StateMachine2E01A3A5" - }, - "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n \n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@: @@$context.resourcePath@@\")\n #end\n \n #set($requestContext = \"@@requestContext@@: {@@accountId@@:@@$context.identity.accountId@@,@@apiId@@:@@$context.apiId@@}\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n" - ] - ] - } - }, - "Type": "AWS", - "Uri": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":apigateway:", - { - "Ref": "AWS::Region" - }, - ":states:action/StartSyncExecution" - ] - ] - } - }, - "MethodResponses": [ - { - "ResponseModels": { - "application/json": "Empty" - }, - "StatusCode": "200" - }, - { - "ResponseModels": { - "application/json": "Error" - }, - "StatusCode": "400" - }, - { - "ResponseModels": { - "application/json": "Error" - }, - "StatusCode": "500" - } - ] - } - }, - "deployment33381975b5dafda9a97138f301ea25da405640e8": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "StepFunctionsRestApiC6E3E883" - } - }, - "DependsOn": [ - "StepFunctionsRestApiANY7699CA92" - ] - }, - "stage0661E4AC": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "StepFunctionsRestApiC6E3E883" - }, - "DeploymentId": { - "Ref": "deployment33381975b5dafda9a97138f301ea25da405640e8" - }, - "StageName": "prod" - } - } - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-multiple-properties.ts b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-multiple-properties.ts deleted file mode 100644 index f3b8629509675..0000000000000 --- a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-multiple-properties.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as cdk from '@aws-cdk/core'; -import { Construct } from 'constructs'; -import * as apigw from '../lib'; - -/** - * Stack verification steps: - * * `curl -X POST 'https://.execute-api..amazonaws.com/prod' \ - * * -d '{"key":"Hello"}' -H 'Content-Type: application/json'` - * The above should return a "Hello" response - */ - -class StepFunctionsRestApiDeploymentStack extends cdk.Stack { - constructor(scope: Construct) { - super(scope, 'StepFunctionsRestApiDeploymentStack'); - - const passTask = new sfn.Pass(this, 'PassTask', { - result: { value: 'Hello' }, - }); - - const stateMachine = new sfn.StateMachine(this, 'StateMachine', { - definition: passTask, - stateMachineType: sfn.StateMachineType.EXPRESS, - }); - - const api = new apigw.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { - deploy: false, - stateMachine: stateMachine, - requestContext: { - accountId: true, - apiId: true, - }, - }); - - api.deploymentStage = new apigw.Stage(this, 'stage', { - deployment: new apigw.Deployment(this, 'deployment', { - api, - }), - }); - } -} - -const app = new cdk.App(); -new StepFunctionsRestApiDeploymentStack(app); -app.synth(); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-single-property.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-single-property.expected.json deleted file mode 100644 index 1ac39fd9b121c..0000000000000 --- a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-single-property.expected.json +++ /dev/null @@ -1,261 +0,0 @@ -{ - "Resources": { - "StateMachineRoleB840431D": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": { - "Fn::Join": [ - "", - [ - "states.", - { - "Ref": "AWS::Region" - }, - ".amazonaws.com" - ] - ] - } - } - } - ], - "Version": "2012-10-17" - } - } - }, - "StateMachine2E01A3A5": { - "Type": "AWS::StepFunctions::StateMachine", - "Properties": { - "RoleArn": { - "Fn::GetAtt": [ - "StateMachineRoleB840431D", - "Arn" - ] - }, - "DefinitionString": "{\"StartAt\":\"PassTask\",\"States\":{\"PassTask\":{\"Type\":\"Pass\",\"Result\":\"Hello\",\"End\":true}}}", - "StateMachineType": "EXPRESS" - }, - "DependsOn": [ - "StateMachineRoleB840431D" - ] - }, - "StartSyncExecutionRoleDE73CB90": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "apigateway.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - } - } - }, - "StartSyncExecutionRoleDefaultPolicy5A5803F8": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": "states:StartSyncExecution", - "Effect": "Allow", - "Resource": { - "Ref": "StateMachine2E01A3A5" - } - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "StartSyncExecutionRoleDefaultPolicy5A5803F8", - "Roles": [ - { - "Ref": "StartSyncExecutionRoleDE73CB90" - } - ] - } - }, - "StepFunctionsRestApiC6E3E883": { - "Type": "AWS::ApiGateway::RestApi", - "Properties": { - "Name": "StepFunctionsRestApi" - } - }, - "StepFunctionsRestApiCloudWatchRoleB06ACDB9": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "apigateway.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "ManagedPolicyArns": [ - { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" - ] - ] - } - ] - } - }, - "StepFunctionsRestApiAccountBD0CCC0E": { - "Type": "AWS::ApiGateway::Account", - "Properties": { - "CloudWatchRoleArn": { - "Fn::GetAtt": [ - "StepFunctionsRestApiCloudWatchRoleB06ACDB9", - "Arn" - ] - } - }, - "DependsOn": [ - "StepFunctionsRestApiC6E3E883" - ] - }, - "StepFunctionsRestApiANY7699CA92": { - "Type": "AWS::ApiGateway::Method", - "Properties": { - "HttpMethod": "ANY", - "ResourceId": { - "Fn::GetAtt": [ - "StepFunctionsRestApiC6E3E883", - "RootResourceId" - ] - }, - "RestApiId": { - "Ref": "StepFunctionsRestApiC6E3E883" - }, - "AuthorizationType": "NONE", - "Integration": { - "Credentials": { - "Fn::GetAtt": [ - "StartSyncExecutionRoleDE73CB90", - "Arn" - ] - }, - "IntegrationHttpMethod": "POST", - "IntegrationResponses": [ - { - "ResponseTemplates": { - "application/json": "#set($inputRoot = $input.path('$'))\n#if($input.path('$.status').toString().equals(\"FAILED\"))\n#set($context.responseOverride.status = 500)\n{\n\"error\": \"$input.path('$.error')\"\n\"cause\": \"$input.path('$.cause')\"\n}\n#else\n$input.path('$.output')\n#end" - }, - "StatusCode": "200" - }, - { - "ResponseTemplates": { - "application/json": "{\n \"error\": \"Bad request!\"\n }" - }, - "SelectionPattern": "4\\d{2}", - "StatusCode": "400" - }, - { - "ResponseTemplates": { - "application/json": "\"error\": $input.path('$.error')" - }, - "SelectionPattern": "5\\d{2}", - "StatusCode": "500" - } - ], - "PassthroughBehavior": "NEVER", - "RequestTemplates": { - "application/json": { - "Fn::Join": [ - "", - [ - "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = true)\n#set($includePath = true)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", - { - "Ref": "StateMachine2E01A3A5" - }, - "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n \n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@: @@$context.resourcePath@@\")\n #end\n \n #set($requestContext = \"@@requestContext@@: {@@accountId@@:@@$context.identity.accountId@@}\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n" - ] - ] - } - }, - "Type": "AWS", - "Uri": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":apigateway:", - { - "Ref": "AWS::Region" - }, - ":states:action/StartSyncExecution" - ] - ] - } - }, - "MethodResponses": [ - { - "ResponseModels": { - "application/json": "Empty" - }, - "StatusCode": "200" - }, - { - "ResponseModels": { - "application/json": "Error" - }, - "StatusCode": "400" - }, - { - "ResponseModels": { - "application/json": "Error" - }, - "StatusCode": "500" - } - ] - } - }, - "deployment33381975b5dafda9a97138f301ea25da405640e8": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "StepFunctionsRestApiC6E3E883" - } - }, - "DependsOn": [ - "StepFunctionsRestApiANY7699CA92" - ] - }, - "stage0661E4AC": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "StepFunctionsRestApiC6E3E883" - }, - "DeploymentId": { - "Ref": "deployment33381975b5dafda9a97138f301ea25da405640e8" - }, - "StageName": "prod" - } - } - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-single-property.ts b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-single-property.ts deleted file mode 100644 index 95e3810aa5c77..0000000000000 --- a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api-request-context-single-property.ts +++ /dev/null @@ -1,44 +0,0 @@ -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as cdk from '@aws-cdk/core'; -import { Construct } from 'constructs'; -import * as apigw from '../lib'; - -/** - * Stack verification steps: - * * `curl -X POST 'https://.execute-api..amazonaws.com/prod' \ - * * -d '{"key":"Hello"}' -H 'Content-Type: application/json'` - * The above should return a "Hello" response - */ - -class StepFunctionsRestApiDeploymentStack extends cdk.Stack { - constructor(scope: Construct) { - super(scope, 'StepFunctionsRestApiDeploymentStack'); - - const passTask = new sfn.Pass(this, 'PassTask', { - result: { value: 'Hello' }, - }); - - const stateMachine = new sfn.StateMachine(this, 'StateMachine', { - definition: passTask, - stateMachineType: sfn.StateMachineType.EXPRESS, - }); - - const api = new apigw.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { - deploy: false, - stateMachine: stateMachine, - requestContext: { - accountId: true, - }, - }); - - api.deploymentStage = new apigw.Stage(this, 'stage', { - deployment: new apigw.Deployment(this, 'deployment', { - api, - }), - }); - } -} - -const app = new cdk.App(); -new StepFunctionsRestApiDeploymentStack(app); -app.synth(); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.expected.json index 51d4c989e9564..a6fa6e231a674 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.expected.json @@ -185,11 +185,11 @@ "Fn::Join": [ "", [ - "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = true)\n#set($includePath = true)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", + "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = true)\n#set($includeQueryString = false)\n#set($includePath = false)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", { "Ref": "StateMachine2E01A3A5" }, - "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n \n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@: @@$context.resourcePath@@\")\n #end\n \n #set($requestContext = \"\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n" + "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@:{\")\n #foreach($paramName in $allParams.path.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.path.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n \n #set($requestContext = \"{@@accountId@@:@@$context.identity.accountId@@,@@userArn@@:@@$context.identity.userArn@@}\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString @@requestContext@@: $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n" ] ] } diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.ts b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.ts index 6363456a49e64..c5639e2df167b 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.ts @@ -26,6 +26,13 @@ class StepFunctionsRestApiDeploymentStack extends cdk.Stack { const api = new apigw.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { deploy: false, stateMachine: stateMachine, + headers: true, + path: false, + querystring: false, + requestContext: { + accountId: true, + userArn: true, + }, }); api.deploymentStage = new apigw.Stage(this, 'stage', { diff --git a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts index 28dd3f264ef93..7849dca94c5a5 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts @@ -1,6 +1,6 @@ import '@aws-cdk/assert-internal/jest'; import * as sfn from '@aws-cdk/aws-stepfunctions'; -import { StateMachine } from '@aws-cdk/aws-stepfunctions'; +import { StateMachine, StateMachineType } from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; import * as apigw from '../../lib'; @@ -69,7 +69,7 @@ describe('StepFunctions', () => { const { stack, api, stateMachine } = givenSetup(); //WHEN - const integ = new apigw.StepFunctionsSynchronousIntegration(stateMachine); + const integ = new apigw.StepFunctionsExecutionIntegration(stateMachine); api.root.addMethod('GET', integ); //THEN @@ -114,7 +114,7 @@ describe('StepFunctions', () => { { Ref: 'StateMachine2E01A3A5', }, - "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n \n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@: @@$context.resourcePath@@\")\n #end\n \n #set($requestContext = \"\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n", + "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@:{\")\n #foreach($paramName in $allParams.path.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.path.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n \n #set($requestContext = \"\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString @@requestContext@@: $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n", ], ], }, @@ -139,7 +139,7 @@ describe('StepFunctions', () => { stateMachineType: sfn.StateMachineType.EXPRESS, }); - api.root.addMethod('ANY', new apigw.StepFunctionsSynchronousIntegration(stateMachine)); + api.root.addMethod('ANY', new apigw.StepFunctionsExecutionIntegration(stateMachine)); expect(stack).toHaveResource('AWS::ApiGateway::Method', { HttpMethod: 'ANY', @@ -191,7 +191,7 @@ describe('StepFunctions', () => { { Ref: 'StateMachine2E01A3A5', }, - "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n \n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@: @@$context.resourcePath@@\")\n #end\n \n #set($requestContext = \"\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n", + "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@:{\")\n #foreach($paramName in $allParams.path.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.path.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n \n #set($requestContext = \"\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString @@requestContext@@: $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n", ], ], }, @@ -232,7 +232,7 @@ describe('StepFunctions', () => { stateMachineType: sfn.StateMachineType.EXPRESS, }); - const integ = new apigw.StepFunctionsSynchronousIntegration(stateMachine, {}); + const integ = new apigw.StepFunctionsExecutionIntegration(stateMachine); // WHEN const bindResult = integ.bind(method); @@ -247,7 +247,7 @@ describe('StepFunctions', () => { const restapi = new apigw.RestApi(stack, 'RestApi'); const method = restapi.root.addMethod('ANY'); const stateMachine: sfn.IStateMachine = StateMachine.fromStateMachineArn(stack, 'MyStateMachine', 'arn:aws:states:region:account:stateMachine:MyStateMachine'); - const integration = new apigw.StepFunctionsSynchronousIntegration(stateMachine, {}); + const integration = new apigw.StepFunctionsExecutionIntegration(stateMachine, {}); // WHEN const bindResult = integration.bind(method); @@ -256,4 +256,20 @@ describe('StepFunctions', () => { // should be a literal string. expect(bindResult?.deploymentToken).toEqual('{"stateMachineName":"StateMachine-c8adc83b19e793491b1c6ea0fd8b46cd9f32e592fc"}'); }); + + test('fails integration if State Machine is not of type EXPRESS', () => { + //GIVEN + const stack = new cdk.Stack(); + const restapi = new apigw.RestApi(stack, 'RestApi'); + const method = restapi.root.addMethod('ANY'); + const stateMachine: sfn.StateMachine = new StateMachine(stack, 'StateMachine', { + definition: new sfn.Pass(stack, 'passTask'), + stateMachineType: StateMachineType.STANDARD, + }); + const integration = new apigw.StepFunctionsExecutionIntegration(stateMachine); + + //WHEN + THEN + expect(() => integration.bind(method)) + .toThrow(/State Machine must be of type "EXPRESS". Please use StateMachineType.EXPRESS as the stateMachineType/); + }); }); diff --git a/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts b/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts index e21d49a40c754..3c50862a9b547 100644 --- a/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts @@ -92,7 +92,7 @@ function getIntegrationResponse() { } describe('Step Functions api', () => { - test('StepFunctionsRestApi defines correct REST API resouces', () => { + test('StepFunctionsRestApi defines correct REST API resources', () => { //GIVEN const { stack, stateMachine } = givenSetup(); @@ -135,7 +135,7 @@ describe('Step Functions api', () => { { Ref: 'StateMachine2E01A3A5', }, - "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n \n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@: @@$context.resourcePath@@\")\n #end\n \n #set($requestContext = \"\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n", + "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@:{\")\n #foreach($paramName in $allParams.path.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.path.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n \n #set($requestContext = \"\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString @@requestContext@@: $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n", ], ], }, @@ -162,7 +162,7 @@ describe('Step Functions api', () => { }); }); - test('StepFunctionsRestApi defines correct REST API resouces with includeRequestContext set to true', () => { + test('StepFunctionsRestApi defines correct REST API resources with includeRequestContext set to true', () => { //GIVEN const { stack, stateMachine } = givenSetup(); @@ -228,7 +228,7 @@ describe('Step Functions api', () => { { Ref: 'StateMachine2E01A3A5', }, - "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n \n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@: @@$context.resourcePath@@\")\n #end\n \n #set($requestContext = \"@@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@@}\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n", + "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@:{\")\n #foreach($paramName in $allParams.path.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.path.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n \n #set($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@@}\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString @@requestContext@@: $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n", ], ], }, @@ -255,7 +255,7 @@ describe('Step Functions api', () => { }); }); - test('StepFunctionsRestApi defines correct REST API resouces with includeRequestContext set to empty', () => { //GIVEN + test('StepFunctionsRestApi defines correct REST API resources with includeRequestContext set to empty', () => { //GIVEN const { stack, stateMachine } = givenSetup(); //WHEN @@ -301,7 +301,7 @@ describe('Step Functions api', () => { { Ref: 'StateMachine2E01A3A5', }, - "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n \n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@: @@$context.resourcePath@@\")\n #end\n \n #set($requestContext = \"\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n", + "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@:{\")\n #foreach($paramName in $allParams.path.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.path.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n \n #set($requestContext = \"\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString @@requestContext@@: $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n", ], ], }, From 3bf2c8baf9334a729dc9bbeb88c7b3a68673508f Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Wed, 24 Nov 2021 15:10:12 +0000 Subject: [PATCH 17/35] change the integration class api --- .../lib/integrations/stepfunctions.ts | 36 +- .../aws-apigateway/lib/stepfunctions-api.ts | 4 +- .../test/integrations/stepfunctions.test.ts | 328 +++++++++--------- 3 files changed, 190 insertions(+), 178 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts index e0d89ab94647a..e74213642bcb4 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts @@ -67,21 +67,31 @@ export interface StepFunctionsExecutionIntegrationOptions extends IntegrationOpt */ readonly headers?: boolean; } + /** - * Integrates a Synchronous Express State Machine from AWS Step Functions to an API Gateway method. - * - * @example - * - * const stateMachine = new stepfunctions.StateMachine(this, 'MyStateMachine', { - * definition: stepfunctions.Chain.start(new stepfunctions.Pass(this, 'Pass')), - * }); - * - * const api = new apigateway.RestApi(this, 'Api', { - * restApiName: 'MyApi', - * }); - * api.root.addMethod('GET', new apigateway.StepFunctionsExecutionIntegration(stateMachine)); + * Options to integrate with various StepFunction API */ -export class StepFunctionsExecutionIntegration extends AwsIntegration { +export class StepFunctionsIntegration { + /** + * Integrates a Synchronous Express State Machine from AWS Step Functions to an API Gateway method. + * + * @example + * + * const stateMachine = new stepfunctions.StateMachine(this, 'MyStateMachine', { + * definition: stepfunctions.Chain.start(new stepfunctions.Pass(this, 'Pass')), + * }); + * + * const api = new apigateway.RestApi(this, 'Api', { + * restApiName: 'MyApi', + * }); + * api.root.addMethod('GET', new apigateway.StepFunctionsIntegration.startExecution(stateMachine)); + */ + public static startExecution(stateMachine: sfn.IStateMachine, options?: StepFunctionsExecutionIntegrationOptions): AwsIntegration { + return new StepFunctionsExecutionIntegration(stateMachine, options); + } +} + +class StepFunctionsExecutionIntegration extends AwsIntegration { private readonly stateMachine: sfn.IStateMachine; constructor(stateMachine: sfn.IStateMachine, options: StepFunctionsExecutionIntegrationOptions = {}) { super({ diff --git a/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts b/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts index f032f1e95cf92..97fa63c154052 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts @@ -3,7 +3,7 @@ import * as sfn from '@aws-cdk/aws-stepfunctions'; import { Construct } from 'constructs'; import { RestApi, RestApiProps } from '.'; import { RequestContext } from './integrations'; -import { StepFunctionsExecutionIntegration } from './integrations/stepfunctions'; +import { StepFunctionsIntegration } from './integrations/stepfunctions'; import { Model } from './model'; /** @@ -63,7 +63,7 @@ export class StepFunctionsRestApi extends RestApi { throw new Error('State Machine must be of type "EXPRESS". Please use StateMachineType.EXPRESS as the stateMachineType'); } - const stepfunctionsIntegration = new StepFunctionsExecutionIntegration(props.stateMachine, { + const stepfunctionsIntegration = StepFunctionsIntegration.startExecution(props.stateMachine, { credentialsRole: role(scope, props), requestContext: props.requestContext, path: props.path?? true, diff --git a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts index 7849dca94c5a5..b88e16e962b46 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts @@ -64,212 +64,214 @@ function getIntegrationResponse() { } describe('StepFunctions', () => { - test('minimal setup', () => { - //GIVEN - const { stack, api, stateMachine } = givenSetup(); + describe('startExecution', () => { + test('minimal setup', () => { + //GIVEN + const { stack, api, stateMachine } = givenSetup(); - //WHEN - const integ = new apigw.StepFunctionsExecutionIntegration(stateMachine); - api.root.addMethod('GET', integ); + //WHEN + const integ = apigw.StepFunctionsIntegration.startExecution(stateMachine); + api.root.addMethod('GET', integ); - //THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { - ResourceId: { - 'Fn::GetAtt': [ - 'myrestapiBAC2BF45', - 'RootResourceId', - ], - }, - RestApiId: { - Ref: 'myrestapiBAC2BF45', - }, - AuthorizationType: 'NONE', - Integration: { - IntegrationHttpMethod: 'POST', - IntegrationResponses: getIntegrationResponse(), - Type: 'AWS', - Uri: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':apigateway:', - { - Ref: 'AWS::Region', - }, - ':states:action/StartSyncExecution', - ], + //THEN + expect(stack).toHaveResource('AWS::ApiGateway::Method', { + ResourceId: { + 'Fn::GetAtt': [ + 'myrestapiBAC2BF45', + 'RootResourceId', ], }, - PassthroughBehavior: 'NEVER', - RequestTemplates: { - 'application/json': { + RestApiId: { + Ref: 'myrestapiBAC2BF45', + }, + AuthorizationType: 'NONE', + Integration: { + IntegrationHttpMethod: 'POST', + IntegrationResponses: getIntegrationResponse(), + Type: 'AWS', + Uri: { 'Fn::Join': [ '', [ - "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = true)\n#set($includePath = true)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", + 'arn:', { - Ref: 'StateMachine2E01A3A5', + Ref: 'AWS::Partition', }, - "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@:{\")\n #foreach($paramName in $allParams.path.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.path.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n \n #set($requestContext = \"\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString @@requestContext@@: $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n", + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':states:action/StartSyncExecution', ], ], }, + PassthroughBehavior: 'NEVER', + RequestTemplates: { + 'application/json': { + 'Fn::Join': [ + '', + [ + "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = true)\n#set($includePath = true)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", + { + Ref: 'StateMachine2E01A3A5', + }, + "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@:{\")\n #foreach($paramName in $allParams.path.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.path.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n \n #set($requestContext = \"\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString @@requestContext@@: $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n", + ], + ], + }, + }, }, - }, + }); }); - }); - test('works for imported RestApi', () => { - const stack = new cdk.Stack(); - const api = apigw.RestApi.fromRestApiAttributes(stack, 'RestApi', { - restApiId: 'imported-rest-api-id', - rootResourceId: 'imported-root-resource-id', - }); + test('works for imported RestApi', () => { + const stack = new cdk.Stack(); + const api = apigw.RestApi.fromRestApiAttributes(stack, 'RestApi', { + restApiId: 'imported-rest-api-id', + rootResourceId: 'imported-root-resource-id', + }); - const passTask = new sfn.Pass(stack, 'passTask', { - inputPath: '$.somekey', - }); + const passTask = new sfn.Pass(stack, 'passTask', { + inputPath: '$.somekey', + }); - const stateMachine: sfn.IStateMachine = new StateMachine(stack, 'StateMachine', { - definition: passTask, - stateMachineType: sfn.StateMachineType.EXPRESS, - }); + const stateMachine: sfn.IStateMachine = new StateMachine(stack, 'StateMachine', { + definition: passTask, + stateMachineType: sfn.StateMachineType.EXPRESS, + }); - api.root.addMethod('ANY', new apigw.StepFunctionsExecutionIntegration(stateMachine)); + api.root.addMethod('ANY', apigw.StepFunctionsIntegration.startExecution(stateMachine)); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { - HttpMethod: 'ANY', - ResourceId: 'imported-root-resource-id', - RestApiId: 'imported-rest-api-id', - AuthorizationType: 'NONE', - Integration: { - IntegrationHttpMethod: 'POST', - IntegrationResponses: [ - { - 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', - ].join('\n'), + expect(stack).toHaveResource('AWS::ApiGateway::Method', { + HttpMethod: 'ANY', + ResourceId: 'imported-root-resource-id', + RestApiId: 'imported-rest-api-id', + AuthorizationType: 'NONE', + Integration: { + IntegrationHttpMethod: 'POST', + IntegrationResponses: [ + { + 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', + ].join('\n'), + }, + StatusCode: '200', }, - StatusCode: '200', - }, - { - ResponseTemplates: { - 'application/json': '{\n "error": "Bad request!"\n }', + { + ResponseTemplates: { + 'application/json': '{\n "error": "Bad request!"\n }', + }, + SelectionPattern: '4\\d{2}', + StatusCode: '400', }, - SelectionPattern: '4\\d{2}', - StatusCode: '400', - }, - { - ResponseTemplates: { - 'application/json': "\"error\": $input.path('$.error')", + { + ResponseTemplates: { + 'application/json': "\"error\": $input.path('$.error')", + }, + SelectionPattern: '5\\d{2}', + StatusCode: '500', + }, + ], + PassthroughBehavior: 'NEVER', + RequestTemplates: { + 'application/json': { + 'Fn::Join': [ + '', + [ + "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = true)\n#set($includePath = true)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", + { + Ref: 'StateMachine2E01A3A5', + }, + "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@:{\")\n #foreach($paramName in $allParams.path.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.path.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n \n #set($requestContext = \"\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString @@requestContext@@: $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n", + ], + ], }, - SelectionPattern: '5\\d{2}', - StatusCode: '500', }, - ], - PassthroughBehavior: 'NEVER', - RequestTemplates: { - 'application/json': { + Type: 'AWS', + Uri: { 'Fn::Join': [ '', [ - "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = true)\n#set($includePath = true)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', { - Ref: 'StateMachine2E01A3A5', + Ref: 'AWS::Region', }, - "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@:{\")\n #foreach($paramName in $allParams.path.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.path.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n \n #set($requestContext = \"\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString @@requestContext@@: $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n", + ':states:action/StartSyncExecution', ], ], }, }, - Type: 'AWS', - Uri: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':apigateway:', - { - Ref: 'AWS::Region', - }, - ':states:action/StartSyncExecution', - ], - ], - }, - }, + }); }); - }); - test('fingerprint is not computed when stateMachineName is not specified', () => { - // GIVEN - const stack = new cdk.Stack(); - const restapi = new apigw.RestApi(stack, 'RestApi'); - const method = restapi.root.addMethod('ANY'); + test('fingerprint is not computed when stateMachineName is not specified', () => { + // GIVEN + const stack = new cdk.Stack(); + const restapi = new apigw.RestApi(stack, 'RestApi'); + const method = restapi.root.addMethod('ANY'); - const passTask = new sfn.Pass(stack, 'passTask', { - inputPath: '$.somekey', - }); - - const stateMachine: sfn.IStateMachine = new StateMachine(stack, 'StateMachine', { - definition: passTask, - stateMachineType: sfn.StateMachineType.EXPRESS, - }); + const passTask = new sfn.Pass(stack, 'passTask', { + inputPath: '$.somekey', + }); - const integ = new apigw.StepFunctionsExecutionIntegration(stateMachine); + const stateMachine: sfn.IStateMachine = new StateMachine(stack, 'StateMachine', { + definition: passTask, + stateMachineType: sfn.StateMachineType.EXPRESS, + }); - // WHEN - const bindResult = integ.bind(method); + const integ = apigw.StepFunctionsIntegration.startExecution(stateMachine); - // THEN - expect(bindResult?.deploymentToken).toBeUndefined(); - }); + // WHEN + const bindResult = integ.bind(method); - test('bind works for integration with imported State Machine', () => { - // GIVEN - const stack = new cdk.Stack(); - const restapi = new apigw.RestApi(stack, 'RestApi'); - const method = restapi.root.addMethod('ANY'); - const stateMachine: sfn.IStateMachine = StateMachine.fromStateMachineArn(stack, 'MyStateMachine', 'arn:aws:states:region:account:stateMachine:MyStateMachine'); - const integration = new apigw.StepFunctionsExecutionIntegration(stateMachine, {}); + // THEN + expect(bindResult?.deploymentToken).toBeUndefined(); + }); - // WHEN - const bindResult = integration.bind(method); + test('bind works for integration with imported State Machine', () => { + // GIVEN + const stack = new cdk.Stack(); + const restapi = new apigw.RestApi(stack, 'RestApi'); + const method = restapi.root.addMethod('ANY'); + const stateMachine: sfn.IStateMachine = StateMachine.fromStateMachineArn(stack, 'MyStateMachine', 'arn:aws:states:region:account:stateMachine:MyStateMachine'); + const integration = apigw.StepFunctionsIntegration.startExecution(stateMachine, {}); - // the deployment token should be defined since the function name - // should be a literal string. - expect(bindResult?.deploymentToken).toEqual('{"stateMachineName":"StateMachine-c8adc83b19e793491b1c6ea0fd8b46cd9f32e592fc"}'); - }); + // WHEN + const bindResult = integration.bind(method); - test('fails integration if State Machine is not of type EXPRESS', () => { - //GIVEN - const stack = new cdk.Stack(); - const restapi = new apigw.RestApi(stack, 'RestApi'); - const method = restapi.root.addMethod('ANY'); - const stateMachine: sfn.StateMachine = new StateMachine(stack, 'StateMachine', { - definition: new sfn.Pass(stack, 'passTask'), - stateMachineType: StateMachineType.STANDARD, + // the deployment token should be defined since the function name + // should be a literal string. + expect(bindResult?.deploymentToken).toEqual('{"stateMachineName":"StateMachine-c8adc83b19e793491b1c6ea0fd8b46cd9f32e592fc"}'); }); - const integration = new apigw.StepFunctionsExecutionIntegration(stateMachine); - //WHEN + THEN - expect(() => integration.bind(method)) - .toThrow(/State Machine must be of type "EXPRESS". Please use StateMachineType.EXPRESS as the stateMachineType/); + test('fails integration if State Machine is not of type EXPRESS', () => { + //GIVEN + const stack = new cdk.Stack(); + const restapi = new apigw.RestApi(stack, 'RestApi'); + const method = restapi.root.addMethod('ANY'); + const stateMachine: sfn.StateMachine = new StateMachine(stack, 'StateMachine', { + definition: new sfn.Pass(stack, 'passTask'), + stateMachineType: StateMachineType.STANDARD, + }); + const integration = apigw.StepFunctionsIntegration.startExecution(stateMachine); + + //WHEN + THEN + expect(() => integration.bind(method)) + .toThrow(/State Machine must be of type "EXPRESS". Please use StateMachineType.EXPRESS as the stateMachineType/); + }); }); }); From 7d950b14bf3c8581d7e49e7964980e34bf0659a4 Mon Sep 17 00:00:00 2001 From: diegotry <33488603+diegotry@users.noreply.github.com> Date: Wed, 24 Nov 2021 07:44:04 -0800 Subject: [PATCH 18/35] Doc update Co-authored-by: Niranjan Jayakar --- packages/@aws-cdk/aws-apigateway/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index c0bef4bb13f9e..6c555e192dd6c 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -119,7 +119,10 @@ Invoking the endpoint with any HTTP method (`GET`, `POST`, `PUT`, `DELETE`, ...) If the execution fails, an HTTP `500` response is returned with the `error` and `cause` from the execution output as the Response Body. If the request is invalid (ex. bad execution input) HTTP code `400` is returned. -The API response is mapped to the state machine execution `output` field. AWS Step Functions [StartSyncExecution](https://docs.aws.amazon.com/step-functions/latest/apireference/API_StartSyncExecution.html#API_StartSyncExecution_ResponseSyntax) response includes information about billing, AWS Account ID, resource ARNs that are not returned to the caller. In case of failures, the fields `error` and `cause` are returned as part of the response. +The response from the invocation contains only the `output` field from the +[StartSyncExecution](https://docs.aws.amazon.com/step-functions/latest/apireference/API_StartSyncExecution.html#API_StartSyncExecution_ResponseSyntax) API. +In case of failures, the fields `error` and `cause` are returned as part of the response. +Other metadata such as billing details, AWS account ID and resource ARNs are not returned in the API response. By default, a `prod` stage is provisioned. From 2f105769a18e5e94dbb6acc2e3e426e3c4bc7a93 Mon Sep 17 00:00:00 2001 From: diegotry <33488603+diegotry@users.noreply.github.com> Date: Wed, 24 Nov 2021 07:44:26 -0800 Subject: [PATCH 19/35] Doc update Co-authored-by: Niranjan Jayakar --- packages/@aws-cdk/aws-apigateway/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 6c555e192dd6c..890856d5397bb 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -184,7 +184,8 @@ AWS Step Functions will receive the following execution input: } ``` -It is possible to combine included/excluded fields. For example, `headers` and the `requestContext` can be included while `path` and `querystring` are excluded through the following configuration: +Additional information around the request such as the request context and headers can be included as part of the input +forwarded to the state machine. The following example enables headers to be included in the input but not query string. ```ts const stateMachineDefinition = new stepfunctions.Pass(this, 'PassState'); From dc3b54f765861a739f9e17d3d92fd1d24ae9e6ea Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Wed, 24 Nov 2021 15:45:24 +0000 Subject: [PATCH 20/35] rename interface --- .../aws-apigateway/lib/integrations/stepfunctions.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts index e74213642bcb4..717ca8b4156d7 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts @@ -10,7 +10,7 @@ import { AwsIntegration } from './aws'; /** * Options when configuring Step Functions synchronous integration with Rest API */ -export interface StepFunctionsExecutionIntegrationOptions extends IntegrationOptions { +export interface StepFunctionsStartExecutionOptions extends IntegrationOptions { /** * Which details of the incoming request must be passed onto the underlying state machine, @@ -86,14 +86,14 @@ export class StepFunctionsIntegration { * }); * api.root.addMethod('GET', new apigateway.StepFunctionsIntegration.startExecution(stateMachine)); */ - public static startExecution(stateMachine: sfn.IStateMachine, options?: StepFunctionsExecutionIntegrationOptions): AwsIntegration { + public static startExecution(stateMachine: sfn.IStateMachine, options?: StepFunctionsStartExecutionOptions): AwsIntegration { return new StepFunctionsExecutionIntegration(stateMachine, options); } } class StepFunctionsExecutionIntegration extends AwsIntegration { private readonly stateMachine: sfn.IStateMachine; - constructor(stateMachine: sfn.IStateMachine, options: StepFunctionsExecutionIntegrationOptions = {}) { + constructor(stateMachine: sfn.IStateMachine, options: StepFunctionsStartExecutionOptions = {}) { super({ service: 'states', action: 'StartSyncExecution', @@ -209,7 +209,7 @@ function integrationResponse() { * @param options * @returns requestTemplate */ -function requestTemplates(stateMachine: sfn.IStateMachine, options: StepFunctionsExecutionIntegrationOptions) { +function requestTemplates(stateMachine: sfn.IStateMachine, options: StepFunctionsStartExecutionOptions) { const templateStr = templateString(stateMachine, options); const requestTemplate: { [contentType:string] : string } = @@ -231,7 +231,7 @@ function requestTemplates(stateMachine: sfn.IStateMachine, options: StepFunction */ function templateString( stateMachine: sfn.IStateMachine, - options: StepFunctionsExecutionIntegrationOptions): string { + options: StepFunctionsStartExecutionOptions): string { let templateStr: string; let requestContextStr = ''; From 8ebf0c7e4f3cdb6deb06b88ed36d20c309635e4e Mon Sep 17 00:00:00 2001 From: diegotry <33488603+diegotry@users.noreply.github.com> Date: Wed, 24 Nov 2021 07:49:04 -0800 Subject: [PATCH 21/35] Doc update Co-authored-by: Niranjan Jayakar --- .../aws-apigateway/lib/integrations/request-context.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/request-context.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/request-context.ts index ee7e7fa617505..12847584f5958 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/request-context.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/request-context.ts @@ -1,9 +1,8 @@ /** - * This interface exposes what properties should be included in the `requestContext` + * Configure what must be included in the `requestContext` * - * More details can be found at mapping templates documentation - * - * https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html + * More details can be found at mapping templates documentation. + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html */ export interface RequestContext { /** From de43130e4a5e741d7c66dbdbf6daa1bca9c83cee Mon Sep 17 00:00:00 2001 From: diegotry <33488603+diegotry@users.noreply.github.com> Date: Wed, 24 Nov 2021 07:49:22 -0800 Subject: [PATCH 22/35] Doc update Co-authored-by: Niranjan Jayakar --- packages/@aws-cdk/aws-apigateway/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 890856d5397bb..eb32e591291c6 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -207,7 +207,7 @@ new apigateway.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { }); ``` -When the endpoint is invoked at path '/' using the HTTP GET method as below: +In such a case, when the endpoint is invoked as below: ```bash curl -X GET https://example.com/ From 2cc14dc0d29695a59f055ada8fa8352101523682 Mon Sep 17 00:00:00 2001 From: diegotry <33488603+diegotry@users.noreply.github.com> Date: Wed, 24 Nov 2021 07:59:18 -0800 Subject: [PATCH 23/35] Update packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts Co-authored-by: Niranjan Jayakar --- .../aws-apigateway/test/integrations/stepfunctions.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts index b88e16e962b46..7d1e2005c3023 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts @@ -63,7 +63,7 @@ function getIntegrationResponse() { return integResponse; } -describe('StepFunctions', () => { +describe('StepFunctionsExecutionIntegration', () => { describe('startExecution', () => { test('minimal setup', () => { //GIVEN From d2ad11dea4ac527605e774b16c72e2c5734cbbe3 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Wed, 24 Nov 2021 17:04:50 +0000 Subject: [PATCH 24/35] fix snippet --- .../@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts index 717ca8b4156d7..b3a26b5f4f787 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts @@ -84,7 +84,7 @@ export class StepFunctionsIntegration { * const api = new apigateway.RestApi(this, 'Api', { * restApiName: 'MyApi', * }); - * api.root.addMethod('GET', new apigateway.StepFunctionsIntegration.startExecution(stateMachine)); + * api.root.addMethod('GET', apigateway.StepFunctionsIntegration.startExecution(stateMachine)); */ public static startExecution(stateMachine: sfn.IStateMachine, options?: StepFunctionsStartExecutionOptions): AwsIntegration { return new StepFunctionsExecutionIntegration(stateMachine, options); From 1ca20ea4cd897c1984373d6268e879ea593c749d Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Wed, 24 Nov 2021 17:10:08 +0000 Subject: [PATCH 25/35] Revert "rename interface" This reverts commit dc3b54f765861a739f9e17d3d92fd1d24ae9e6ea. --- .../aws-apigateway/lib/integrations/stepfunctions.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts index b3a26b5f4f787..0d8618d64b649 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts @@ -10,7 +10,7 @@ import { AwsIntegration } from './aws'; /** * Options when configuring Step Functions synchronous integration with Rest API */ -export interface StepFunctionsStartExecutionOptions extends IntegrationOptions { +export interface StepFunctionsExecutionIntegrationOptions extends IntegrationOptions { /** * Which details of the incoming request must be passed onto the underlying state machine, @@ -86,14 +86,14 @@ export class StepFunctionsIntegration { * }); * api.root.addMethod('GET', apigateway.StepFunctionsIntegration.startExecution(stateMachine)); */ - public static startExecution(stateMachine: sfn.IStateMachine, options?: StepFunctionsStartExecutionOptions): AwsIntegration { + public static startExecution(stateMachine: sfn.IStateMachine, options?: StepFunctionsExecutionIntegrationOptions): AwsIntegration { return new StepFunctionsExecutionIntegration(stateMachine, options); } } class StepFunctionsExecutionIntegration extends AwsIntegration { private readonly stateMachine: sfn.IStateMachine; - constructor(stateMachine: sfn.IStateMachine, options: StepFunctionsStartExecutionOptions = {}) { + constructor(stateMachine: sfn.IStateMachine, options: StepFunctionsExecutionIntegrationOptions = {}) { super({ service: 'states', action: 'StartSyncExecution', @@ -209,7 +209,7 @@ function integrationResponse() { * @param options * @returns requestTemplate */ -function requestTemplates(stateMachine: sfn.IStateMachine, options: StepFunctionsStartExecutionOptions) { +function requestTemplates(stateMachine: sfn.IStateMachine, options: StepFunctionsExecutionIntegrationOptions) { const templateStr = templateString(stateMachine, options); const requestTemplate: { [contentType:string] : string } = @@ -231,7 +231,7 @@ function requestTemplates(stateMachine: sfn.IStateMachine, options: StepFunction */ function templateString( stateMachine: sfn.IStateMachine, - options: StepFunctionsStartExecutionOptions): string { + options: StepFunctionsExecutionIntegrationOptions): string { let templateStr: string; let requestContextStr = ''; From e4b9ff58c94666c7f70c267d91059437c96c9d8b Mon Sep 17 00:00:00 2001 From: Diego Santiviago Date: Wed, 24 Nov 2021 10:17:40 -0800 Subject: [PATCH 26/35] Update fields documentation (requestContext, querystring, path and headers) --- .../aws-apigateway/lib/stepfunctions-api.ts | 45 +++++++++++++++---- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts b/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts index 97fa63c154052..52a3d1513f877 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts @@ -20,31 +20,58 @@ export interface StepFunctionsRestApiProps extends RestApiProps { readonly stateMachine: sfn.IStateMachine; /** - * You can add requestContext (similar to input requestContext from lambda input) - * to the input. The 'requestContext' parameter includes account ID, user identity, etc. - * that can be used by customers that want to know the identity of authorized users on - * the state machine side. You can individually select the keys you want by setting them to true. - * The following code defines a REST API like above but also adds 'requestContext' to the input - * of the State Machine: + * Which details of the incoming request must be passed onto the underlying state machine, + * such as, account id, user identity, request id, etc. The execution input will include a new key `requestContext`: + * + * { + * "body": {}, + * "requestContext": { + * "key": "value" + * } + * } * * @default - all parameters within request context will be set as false */ readonly requestContext?: RequestContext; /** - * Check if querystring is to be included inside the execution input + * Check if querystring is to be included inside the execution input. The execution input will include a new key `queryString`: + * + * { + * "body": {}, + * "querystring": { + * "key": "value" + * } + * } + * * @default true */ readonly querystring?: boolean; /** - * Check if path is to be included inside the execution input + * Check if path is to be included inside the execution input. The execution input will include a new key `path`: + * + * { + * "body": {}, + * "path": { + * "resourceName": "resourceValue" + * } + * } + * * @default true */ readonly path?: boolean; /** - * Check if header is to be included inside the execution input + * Check if header is to be included inside the execution input. The execution input will include a new key `headers`: + * + * { + * "body": {}, + * "headers": { + * "header1": "value", + * "header2": "value" + * } + * } * @default false */ readonly headers?: boolean; From ede456faf2e3ea61c9e46572296e59a7454bf888 Mon Sep 17 00:00:00 2001 From: Diego Santiviago Date: Wed, 24 Nov 2021 10:18:07 -0800 Subject: [PATCH 27/35] Update code sample to use path as an object instead of strung --- .../@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts index 0d8618d64b649..339e7b2949198 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts @@ -46,7 +46,9 @@ export interface StepFunctionsExecutionIntegrationOptions extends IntegrationOpt * * { * "body": {}, - * "path": "/" + * "path": { + * "resourceName": "resourceValue" + * } * } * * @default true From c8b1630d67efd71742ce819fc9a455942872bc65 Mon Sep 17 00:00:00 2001 From: Diego Santiviago Date: Wed, 24 Nov 2021 10:18:37 -0800 Subject: [PATCH 28/35] Move functions to the end of the file --- .../test/stepfunctions-api.test.ts | 174 +++++++++--------- 1 file changed, 87 insertions(+), 87 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts b/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts index 3c50862a9b547..0bbcdc599f01d 100644 --- a/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts @@ -4,93 +4,6 @@ import { StateMachine } from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; import * as apigw from '../lib'; -function givenSetup() { - const stack = new cdk.Stack(); - - const passTask = new sfn.Pass(stack, 'passTask', { - inputPath: '$.somekey', - }); - - const stateMachine: sfn.IStateMachine = new StateMachine(stack, 'StateMachine', { - definition: passTask, - stateMachineType: sfn.StateMachineType.EXPRESS, - }); - - return { stack, stateMachine }; -} - -function whenCondition(stack:cdk.Stack, stateMachine: sfn.IStateMachine) { - const api = new apigw.StepFunctionsRestApi(stack, 'StepFunctionsRestApi', { stateMachine: stateMachine }); - return api; -} - -function getMethodResponse() { - return [ - { - StatusCode: '200', - ResponseModels: { - 'application/json': 'Empty', - }, - }, - { - StatusCode: '400', - ResponseModels: { - 'application/json': 'Error', - }, - }, - { - StatusCode: '500', - ResponseModels: { - 'application/json': 'Error', - }, - }, - ]; -} - -function getIntegrationResponse() { - const errorResponse = [ - { - SelectionPattern: '4\\d{2}', - StatusCode: '400', - ResponseTemplates: { - 'application/json': `{ - "error": "Bad request!" - }`, - }, - }, - { - 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', - ].join('\n'), - }, - }, - ...errorResponse, - ]; - - return integResponse; -} - describe('Step Functions api', () => { test('StepFunctionsRestApi defines correct REST API resources', () => { //GIVEN @@ -417,3 +330,90 @@ describe('Step Functions api', () => { }); }); }); + +function givenSetup() { + const stack = new cdk.Stack(); + + const passTask = new sfn.Pass(stack, 'passTask', { + inputPath: '$.somekey', + }); + + const stateMachine: sfn.IStateMachine = new StateMachine(stack, 'StateMachine', { + definition: passTask, + stateMachineType: sfn.StateMachineType.EXPRESS, + }); + + return { stack, stateMachine }; +} + +function whenCondition(stack:cdk.Stack, stateMachine: sfn.IStateMachine) { + const api = new apigw.StepFunctionsRestApi(stack, 'StepFunctionsRestApi', { stateMachine: stateMachine }); + return api; +} + +function getMethodResponse() { + return [ + { + StatusCode: '200', + ResponseModels: { + 'application/json': 'Empty', + }, + }, + { + StatusCode: '400', + ResponseModels: { + 'application/json': 'Error', + }, + }, + { + StatusCode: '500', + ResponseModels: { + 'application/json': 'Error', + }, + }, + ]; +} + +function getIntegrationResponse() { + const errorResponse = [ + { + SelectionPattern: '4\\d{2}', + StatusCode: '400', + ResponseTemplates: { + 'application/json': `{ + "error": "Bad request!" + }`, + }, + }, + { + 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', + ].join('\n'), + }, + }, + ...errorResponse, + ]; + + return integResponse; +} From 8aaf67a7f303ea6d4c10ff91ec38b11541dd16f8 Mon Sep 17 00:00:00 2001 From: Diego Santiviago Date: Wed, 24 Nov 2021 17:47:38 -0800 Subject: [PATCH 29/35] Include tests for request context fields inside the StepFunctionsExecutionIntegration --- .../lib/assertions/have-resource-matchers.ts | 4 +- .../test/integrations/stepfunctions.test.ts | 508 ++++++++++++++---- .../test/stepfunctions-api.test.ts | 222 -------- 3 files changed, 402 insertions(+), 332 deletions(-) diff --git a/packages/@aws-cdk/assert-internal/lib/assertions/have-resource-matchers.ts b/packages/@aws-cdk/assert-internal/lib/assertions/have-resource-matchers.ts index deb64b769ff16..7862542da5480 100644 --- a/packages/@aws-cdk/assert-internal/lib/assertions/have-resource-matchers.ts +++ b/packages/@aws-cdk/assert-internal/lib/assertions/have-resource-matchers.ts @@ -243,7 +243,9 @@ function isCallable(x: any): x is ((...args: any[]) => any) { */ export function stringLike(pattern: string): PropertyMatcher { // Replace * with .* in the string, escape the rest and brace with ^...$ - const regex = new RegExp(`^${pattern.split('*').map(escapeRegex).join('.*')}$`); + + // needs rebase from https://github.com/aws/aws-cdk/pull/17692/files + const regex = new RegExp(`^${pattern.split('*').map(escapeRegex).join('.*')}$`, 'm'); return annotateMatcher({ $stringContaining: pattern }, (value: any, failure: InspectionFailure) => { if (typeof value !== 'string') { diff --git a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts index 7d1e2005c3023..468116b544e50 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts @@ -1,68 +1,10 @@ import '@aws-cdk/assert-internal/jest'; +import { stringLike, anything } from '@aws-cdk/assert-internal'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import { StateMachine, StateMachineType } from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; import * as apigw from '../../lib'; -function givenSetup() { - const stack = new cdk.Stack(); - const api = new apigw.RestApi(stack, 'my-rest-api'); - const passTask = new sfn.Pass(stack, 'passTask', { - inputPath: '$.somekey', - }); - - const stateMachine: sfn.IStateMachine = new StateMachine(stack, 'StateMachine', { - definition: passTask, - stateMachineType: sfn.StateMachineType.EXPRESS, - }); - - return { stack, api, stateMachine }; -} - -function getIntegrationResponse() { - const errorResponse = [ - { - SelectionPattern: '4\\d{2}', - StatusCode: '400', - ResponseTemplates: { - 'application/json': `{ - "error": "Bad request!" - }`, - }, - }, - { - 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', - ].join('\n'), - }, - }, - ...errorResponse, - ]; - - return integResponse; -} - describe('StepFunctionsExecutionIntegration', () => { describe('startExecution', () => { test('minimal setup', () => { @@ -124,79 +66,331 @@ describe('StepFunctionsExecutionIntegration', () => { }); }); - test('works for imported RestApi', () => { - const stack = new cdk.Stack(); - const api = apigw.RestApi.fromRestApiAttributes(stack, 'RestApi', { - restApiId: 'imported-rest-api-id', - rootResourceId: 'imported-root-resource-id', + test('headers are NOT included by default', () => { + //GIVEN + const { stack, api, stateMachine } = givenSetup(); + + //WHEN + const integ = apigw.StepFunctionsIntegration.startExecution(stateMachine); + api.root.addMethod('GET', integ); + + expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Integration: { + IntegrationHttpMethod: 'POST', + IntegrationResponses: getIntegrationResponse(), + Type: 'AWS', + Uri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':states:action/StartSyncExecution', + ], + ], + }, + PassthroughBehavior: 'NEVER', + RequestTemplates: { + 'application/json': { + 'Fn::Join': [ + '', + [ + stringLike('*includeHeaders = false*'), + { Ref: 'StateMachine2E01A3A5' }, + anything(), + ], + ], + }, + }, + }, }); + }); - const passTask = new sfn.Pass(stack, 'passTask', { - inputPath: '$.somekey', + test('headers are included when specified by the integration', () => { + //GIVEN + const { stack, api, stateMachine } = givenSetup(); + + //WHEN + const integ = apigw.StepFunctionsIntegration.startExecution(stateMachine, { + headers: true, }); + api.root.addMethod('GET', integ); - const stateMachine: sfn.IStateMachine = new StateMachine(stack, 'StateMachine', { - definition: passTask, - stateMachineType: sfn.StateMachineType.EXPRESS, + expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Integration: { + IntegrationHttpMethod: 'POST', + IntegrationResponses: getIntegrationResponse(), + Type: 'AWS', + Uri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':states:action/StartSyncExecution', + ], + ], + }, + PassthroughBehavior: 'NEVER', + RequestTemplates: { + 'application/json': { + 'Fn::Join': [ + '', + [ + stringLike('*#set($includeHeaders = true)*'), + { Ref: 'StateMachine2E01A3A5' }, + anything(), + ], + ], + }, + }, + }, }); + }); - api.root.addMethod('ANY', apigw.StepFunctionsIntegration.startExecution(stateMachine)); + test('querystring and path are included by default', () => { + //GIVEN + const { stack, api, stateMachine } = givenSetup(); + + //WHEN + const integ = apigw.StepFunctionsIntegration.startExecution(stateMachine); + api.root.addMethod('GET', integ); expect(stack).toHaveResource('AWS::ApiGateway::Method', { - HttpMethod: 'ANY', - ResourceId: 'imported-root-resource-id', - RestApiId: 'imported-rest-api-id', - AuthorizationType: 'NONE', Integration: { IntegrationHttpMethod: 'POST', - IntegrationResponses: [ - { - 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', - ].join('\n'), - }, - StatusCode: '200', + IntegrationResponses: getIntegrationResponse(), + Type: 'AWS', + Uri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':states:action/StartSyncExecution', + ], + ], + }, + PassthroughBehavior: 'NEVER', + RequestTemplates: { + 'application/json': { + 'Fn::Join': [ + '', + [ + stringLike('*#set($includeQueryString = true)*'), + { Ref: 'StateMachine2E01A3A5' }, + anything(), + ], + ], + }, + }, + }, + }); + + expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Integration: { + IntegrationHttpMethod: 'POST', + IntegrationResponses: getIntegrationResponse(), + Type: 'AWS', + Uri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':states:action/StartSyncExecution', + ], + ], + }, + PassthroughBehavior: 'NEVER', + RequestTemplates: { + 'application/json': { + 'Fn::Join': [ + '', + [ + stringLike('*#set($includePath = true)*'), + { Ref: 'StateMachine2E01A3A5' }, + anything(), + ], + ], }, - { - ResponseTemplates: { - 'application/json': '{\n "error": "Bad request!"\n }', - }, - SelectionPattern: '4\\d{2}', - StatusCode: '400', + }, + }, + }); + }); + + test('querystring and path are false when specified by the integration', () => { + //GIVEN + const { stack, api, stateMachine } = givenSetup(); + + //WHEN + const integ = apigw.StepFunctionsIntegration.startExecution(stateMachine, { + querystring: false, + path: false, + }); + api.root.addMethod('GET', integ); + + expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Integration: { + IntegrationHttpMethod: 'POST', + IntegrationResponses: getIntegrationResponse(), + Type: 'AWS', + Uri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':states:action/StartSyncExecution', + ], + ], + }, + PassthroughBehavior: 'NEVER', + RequestTemplates: { + 'application/json': { + 'Fn::Join': [ + '', + [ + stringLike('*#set($includeQueryString = false)*'), + { Ref: 'StateMachine2E01A3A5' }, + anything(), + ], + ], }, - { - ResponseTemplates: { - 'application/json': "\"error\": $input.path('$.error')", - }, - SelectionPattern: '5\\d{2}', - StatusCode: '500', + }, + }, + }); + + expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Integration: { + IntegrationHttpMethod: 'POST', + IntegrationResponses: getIntegrationResponse(), + Type: 'AWS', + Uri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':states:action/StartSyncExecution', + ], + ], + }, + PassthroughBehavior: 'NEVER', + RequestTemplates: { + 'application/json': { + 'Fn::Join': [ + '', + [ + stringLike('*#set($includePath = false)*'), + { Ref: 'StateMachine2E01A3A5' }, + anything(), + ], + ], }, - ], + }, + }, + }); + }); + + test('request context is NOT included by default', () => { + //GIVEN + const { stack, api, stateMachine } = givenSetup(); + + //WHEN + const integ = apigw.StepFunctionsIntegration.startExecution(stateMachine, {}); + api.root.addMethod('GET', integ); + + expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Integration: { + IntegrationHttpMethod: 'POST', + IntegrationResponses: getIntegrationResponse(), + Type: 'AWS', + Uri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':states:action/StartSyncExecution', + ], + ], + }, PassthroughBehavior: 'NEVER', RequestTemplates: { 'application/json': { 'Fn::Join': [ '', [ - "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = true)\n#set($includePath = true)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", - { - Ref: 'StateMachine2E01A3A5', - }, - "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@:{\")\n #foreach($paramName in $allParams.path.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.path.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n \n #set($requestContext = \"\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString @@requestContext@@: $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n", + anything(), + { Ref: 'StateMachine2E01A3A5' }, + stringLike('*#set($requestContext = \"\")*'), ], ], }, }, + }, + }); + }); + + test('request context is included when specified by the integration', () => { + //GIVEN + const { stack, api, stateMachine } = givenSetup(); + + //WHEN + const integ = apigw.StepFunctionsIntegration.startExecution(stateMachine, { + requestContext: { + accountId: true, + }, + }); + api.root.addMethod('GET', integ); + + expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Integration: { + IntegrationHttpMethod: 'POST', + IntegrationResponses: getIntegrationResponse(), Type: 'AWS', Uri: { 'Fn::Join': [ @@ -214,10 +408,47 @@ describe('StepFunctionsExecutionIntegration', () => { ], ], }, + PassthroughBehavior: 'NEVER', + RequestTemplates: { + 'application/json': { + 'Fn::Join': [ + '', + [ + anything(), + { Ref: 'StateMachine2E01A3A5' }, + stringLike('*#set($requestContext = \"{@@accountId@@:@@$context.identity.accountId@@}\"*'), + ], + ], + }, + }, }, }); }); + test('works for imported RestApi', () => { + const stack = new cdk.Stack(); + const api = apigw.RestApi.fromRestApiAttributes(stack, 'RestApi', { + restApiId: 'imported-rest-api-id', + rootResourceId: 'imported-root-resource-id', + }); + + const passTask = new sfn.Pass(stack, 'passTask', { + inputPath: '$.somekey', + }); + + const stateMachine: sfn.IStateMachine = new StateMachine(stack, 'StateMachine', { + definition: passTask, + stateMachineType: sfn.StateMachineType.EXPRESS, + }); + + api.root.addMethod('ANY', apigw.StepFunctionsIntegration.startExecution(stateMachine)); + + expect(stack).toHaveResource('AWS::ApiGateway::Method', { + ResourceId: 'imported-root-resource-id', + RestApiId: 'imported-rest-api-id', + }); + }); + test('fingerprint is not computed when stateMachineName is not specified', () => { // GIVEN const stack = new cdk.Stack(); @@ -275,3 +506,62 @@ describe('StepFunctionsExecutionIntegration', () => { }); }); }); + +function givenSetup() { + const stack = new cdk.Stack(); + const api = new apigw.RestApi(stack, 'my-rest-api'); + const passTask = new sfn.Pass(stack, 'passTask', { + inputPath: '$.somekey', + }); + + const stateMachine: sfn.IStateMachine = new StateMachine(stack, 'StateMachine', { + definition: passTask, + stateMachineType: sfn.StateMachineType.EXPRESS, + }); + + return { stack, api, stateMachine }; +} + +function getIntegrationResponse() { + const errorResponse = [ + { + SelectionPattern: '4\\d{2}', + StatusCode: '400', + ResponseTemplates: { + 'application/json': `{ + "error": "Bad request!" + }`, + }, + }, + { + 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', + ].join('\n'), + }, + }, + ...errorResponse, + ]; + + return integResponse; +} diff --git a/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts b/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts index 0bbcdc599f01d..30a1769b8965b 100644 --- a/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts @@ -75,172 +75,6 @@ describe('Step Functions api', () => { }); }); - test('StepFunctionsRestApi defines correct REST API resources with includeRequestContext set to true', () => { - //GIVEN - const { stack, stateMachine } = givenSetup(); - - //WHEN - const api = new apigw.StepFunctionsRestApi(stack, - 'StepFunctionsRestApi', { - stateMachine: stateMachine, - requestContext: { - accountId: true, - apiId: true, - apiKey: true, - authorizerPrincipalId: true, - caller: true, - cognitoAuthenticationProvider: true, - cognitoAuthenticationType: true, - cognitoIdentityId: true, - cognitoIdentityPoolId: true, - httpMethod: true, - stage: true, - sourceIp: true, - user: true, - userAgent: true, - userArn: true, - requestId: true, - resourceId: true, - resourcePath: true, - }, - }); - - expect(() => { - api.root.addResource('not allowed'); - }).toThrow(); - - //THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { - HttpMethod: 'ANY', - MethodResponses: getMethodResponse(), - AuthorizationType: 'NONE', - RestApiId: { - Ref: 'StepFunctionsRestApiC6E3E883', - }, - ResourceId: { - 'Fn::GetAtt': [ - 'StepFunctionsRestApiC6E3E883', - 'RootResourceId', - ], - }, - Integration: { - Credentials: { - 'Fn::GetAtt': [ - 'StartSyncExecutionRoleDE73CB90', - 'Arn', - ], - }, - IntegrationHttpMethod: 'POST', - IntegrationResponses: getIntegrationResponse(), - RequestTemplates: { - 'application/json': { - 'Fn::Join': [ - '', - [ - "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = true)\n#set($includePath = true)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", - { - Ref: 'StateMachine2E01A3A5', - }, - "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@:{\")\n #foreach($paramName in $allParams.path.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.path.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n \n #set($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@@}\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString @@requestContext@@: $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n", - ], - ], - }, - }, - Type: 'AWS', - Uri: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':apigateway:', - { - Ref: 'AWS::Region', - }, - ':states:action/StartSyncExecution', - ], - ], - }, - PassthroughBehavior: 'NEVER', - }, - }); - }); - - test('StepFunctionsRestApi defines correct REST API resources with includeRequestContext set to empty', () => { //GIVEN - const { stack, stateMachine } = givenSetup(); - - //WHEN - const api = new apigw.StepFunctionsRestApi(stack, - 'StepFunctionsRestApi', { - stateMachine: stateMachine, - requestContext: {}, - }); - - expect(() => { - api.root.addResource('not allowed'); - }).toThrow(); - - //THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { - HttpMethod: 'ANY', - MethodResponses: getMethodResponse(), - AuthorizationType: 'NONE', - RestApiId: { - Ref: 'StepFunctionsRestApiC6E3E883', - }, - ResourceId: { - 'Fn::GetAtt': [ - 'StepFunctionsRestApiC6E3E883', - 'RootResourceId', - ], - }, - Integration: { - Credentials: { - 'Fn::GetAtt': [ - 'StartSyncExecutionRoleDE73CB90', - 'Arn', - ], - }, - IntegrationHttpMethod: 'POST', - IntegrationResponses: getIntegrationResponse(), - RequestTemplates: { - 'application/json': { - 'Fn::Join': [ - '', - [ - "## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = true)\n#set($includePath = true)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"", - { - Ref: 'StateMachine2E01A3A5', - }, - "\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@:{\")\n #foreach($paramName in $allParams.path.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.path.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n \n #set($requestContext = \"\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString @@requestContext@@: $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n", - ], - ], - }, - }, - Type: 'AWS', - Uri: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':apigateway:', - { - Ref: 'AWS::Region', - }, - ':states:action/StartSyncExecution', - ], - ], - }, - PassthroughBehavior: 'NEVER', - }, - }); - }); - test('fails if options.defaultIntegration is set', () => { //GIVEN const { stack, stateMachine } = givenSetup(); @@ -273,62 +107,6 @@ describe('Step Functions api', () => { stateMachine: stateMachine, })).toThrow(/State Machine must be of type "EXPRESS". Please use StateMachineType.EXPRESS as the stateMachineType/); }); - - test('StepFunctionsRestApi defines a REST API with CORS enabled', () => { - const { stack, stateMachine } = givenSetup(); - - //WHEN - new apigw.StepFunctionsRestApi(stack, 'StepFunctionsRestApi', { - stateMachine: stateMachine, - defaultCorsPreflightOptions: { - allowOrigins: ['https://aws.amazon.com'], - allowMethods: ['GET', 'PUT'], - }, - }); - - //THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { - HttpMethod: 'OPTIONS', - ResourceId: { - 'Fn::GetAtt': [ - 'StepFunctionsRestApiC6E3E883', - 'RootResourceId', - ], - }, - RestApiId: { - Ref: 'StepFunctionsRestApiC6E3E883', - }, - AuthorizationType: 'NONE', - Integration: { - IntegrationResponses: [ - { - ResponseParameters: { - 'method.response.header.Access-Control-Allow-Headers': "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", - 'method.response.header.Access-Control-Allow-Origin': "'https://aws.amazon.com'", - 'method.response.header.Vary': "'Origin'", - 'method.response.header.Access-Control-Allow-Methods': "'GET,PUT'", - }, - StatusCode: '204', - }, - ], - RequestTemplates: { - 'application/json': '{ statusCode: 200 }', - }, - Type: 'MOCK', - }, - MethodResponses: [ - { - ResponseParameters: { - 'method.response.header.Access-Control-Allow-Headers': true, - 'method.response.header.Access-Control-Allow-Origin': true, - 'method.response.header.Vary': true, - 'method.response.header.Access-Control-Allow-Methods': true, - }, - StatusCode: '204', - }, - ], - }); - }); }); function givenSetup() { From e501659a834abd41406a6957a6a112ddcc4aa694 Mon Sep 17 00:00:00 2001 From: diegotry <33488603+diegotry@users.noreply.github.com> Date: Wed, 24 Nov 2021 18:54:48 -0800 Subject: [PATCH 30/35] Update packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.ts Co-authored-by: Niranjan Jayakar --- .../@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.ts b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.ts index c5639e2df167b..baa43fb1e97e3 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.ts @@ -40,6 +40,10 @@ class StepFunctionsRestApiDeploymentStack extends cdk.Stack { api, }), }); + + new cdk.CfnOutput(this, 'ApiEndpoint', { + value: api.url, + }); } } From 9a95f460f4a91f104770aba8b5484f2efffb665d Mon Sep 17 00:00:00 2001 From: Diego Santiviago Date: Wed, 24 Nov 2021 21:32:20 -0800 Subject: [PATCH 31/35] Update integ test.exepcted.json --- .../integ.stepfunctions-api.expected.json | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.expected.json index a6fa6e231a674..81e4a643c14e4 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.expected.json @@ -257,5 +257,33 @@ "StageName": "prod" } } + }, + "Outputs": { + "ApiEndpoint": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "StepFunctionsRestApiC6E3E883" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "stage0661E4AC" + }, + "/" + ] + ] + } + } } } \ No newline at end of file From 884397e48f485a4b50af4242f95c0269280c8000 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Thu, 25 Nov 2021 09:51:02 +0000 Subject: [PATCH 32/35] update tests --- .../test/integrations/stepfunctions.test.ts | 176 +----------------- 1 file changed, 8 insertions(+), 168 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts index 468116b544e50..24169ee721093 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts @@ -74,28 +74,8 @@ describe('StepFunctionsExecutionIntegration', () => { const integ = apigw.StepFunctionsIntegration.startExecution(stateMachine); api.root.addMethod('GET', integ); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { Integration: { - IntegrationHttpMethod: 'POST', - IntegrationResponses: getIntegrationResponse(), - Type: 'AWS', - Uri: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':apigateway:', - { - Ref: 'AWS::Region', - }, - ':states:action/StartSyncExecution', - ], - ], - }, - PassthroughBehavior: 'NEVER', RequestTemplates: { 'application/json': { 'Fn::Join': [ @@ -122,28 +102,8 @@ describe('StepFunctionsExecutionIntegration', () => { }); api.root.addMethod('GET', integ); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { Integration: { - IntegrationHttpMethod: 'POST', - IntegrationResponses: getIntegrationResponse(), - Type: 'AWS', - Uri: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':apigateway:', - { - Ref: 'AWS::Region', - }, - ':states:action/StartSyncExecution', - ], - ], - }, - PassthroughBehavior: 'NEVER', RequestTemplates: { 'application/json': { 'Fn::Join': [ @@ -168,28 +128,8 @@ describe('StepFunctionsExecutionIntegration', () => { const integ = apigw.StepFunctionsIntegration.startExecution(stateMachine); api.root.addMethod('GET', integ); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { Integration: { - IntegrationHttpMethod: 'POST', - IntegrationResponses: getIntegrationResponse(), - Type: 'AWS', - Uri: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':apigateway:', - { - Ref: 'AWS::Region', - }, - ':states:action/StartSyncExecution', - ], - ], - }, - PassthroughBehavior: 'NEVER', RequestTemplates: { 'application/json': { 'Fn::Join': [ @@ -205,28 +145,8 @@ describe('StepFunctionsExecutionIntegration', () => { }, }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { Integration: { - IntegrationHttpMethod: 'POST', - IntegrationResponses: getIntegrationResponse(), - Type: 'AWS', - Uri: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':apigateway:', - { - Ref: 'AWS::Region', - }, - ':states:action/StartSyncExecution', - ], - ], - }, - PassthroughBehavior: 'NEVER', RequestTemplates: { 'application/json': { 'Fn::Join': [ @@ -254,28 +174,8 @@ describe('StepFunctionsExecutionIntegration', () => { }); api.root.addMethod('GET', integ); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { Integration: { - IntegrationHttpMethod: 'POST', - IntegrationResponses: getIntegrationResponse(), - Type: 'AWS', - Uri: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':apigateway:', - { - Ref: 'AWS::Region', - }, - ':states:action/StartSyncExecution', - ], - ], - }, - PassthroughBehavior: 'NEVER', RequestTemplates: { 'application/json': { 'Fn::Join': [ @@ -291,28 +191,8 @@ describe('StepFunctionsExecutionIntegration', () => { }, }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { Integration: { - IntegrationHttpMethod: 'POST', - IntegrationResponses: getIntegrationResponse(), - Type: 'AWS', - Uri: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':apigateway:', - { - Ref: 'AWS::Region', - }, - ':states:action/StartSyncExecution', - ], - ], - }, - PassthroughBehavior: 'NEVER', RequestTemplates: { 'application/json': { 'Fn::Join': [ @@ -337,28 +217,8 @@ describe('StepFunctionsExecutionIntegration', () => { const integ = apigw.StepFunctionsIntegration.startExecution(stateMachine, {}); api.root.addMethod('GET', integ); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { Integration: { - IntegrationHttpMethod: 'POST', - IntegrationResponses: getIntegrationResponse(), - Type: 'AWS', - Uri: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':apigateway:', - { - Ref: 'AWS::Region', - }, - ':states:action/StartSyncExecution', - ], - ], - }, - PassthroughBehavior: 'NEVER', RequestTemplates: { 'application/json': { 'Fn::Join': [ @@ -387,28 +247,8 @@ describe('StepFunctionsExecutionIntegration', () => { }); api.root.addMethod('GET', integ); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { Integration: { - IntegrationHttpMethod: 'POST', - IntegrationResponses: getIntegrationResponse(), - Type: 'AWS', - Uri: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':apigateway:', - { - Ref: 'AWS::Region', - }, - ':states:action/StartSyncExecution', - ], - ], - }, - PassthroughBehavior: 'NEVER', RequestTemplates: { 'application/json': { 'Fn::Join': [ From 0bf4637fae990b35c872b6ba2c63b4953378ae40 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Thu, 25 Nov 2021 10:49:48 +0000 Subject: [PATCH 33/35] update snippets --- packages/@aws-cdk/aws-apigateway/README.md | 19 ++++++------------- .../rosetta/stepfunctions.ts-fixture | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 13 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigateway/rosetta/stepfunctions.ts-fixture diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index eb32e591291c6..83e9d04cd31c6 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -136,8 +136,8 @@ The following code defines a REST API that routes all requests to the specified const stateMachineDefinition = new stepfunctions.Pass(this, 'PassState'); const stateMachine: stepfunctions.IStateMachine = new stepfunctions.StateMachine(this, 'StateMachine', { - definition: stateMachineDefinition, - stateMachineType: stepfunctions.StateMachineType.EXPRESS, + definition: stateMachineDefinition, + stateMachineType: stepfunctions.StateMachineType.EXPRESS, }); new apigateway.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { @@ -187,22 +187,15 @@ AWS Step Functions will receive the following execution input: Additional information around the request such as the request context and headers can be included as part of the input forwarded to the state machine. The following example enables headers to be included in the input but not query string. -```ts -const stateMachineDefinition = new stepfunctions.Pass(this, 'PassState'); - -const stateMachine: stepfunctions.IStateMachine = new stepfunctions.StateMachine(this, 'StateMachine', { - definition: stateMachineDefinition, - stateMachineType: stepfunctions.StateMachineType.EXPRESS, -}); - +```ts fixture=stepfunctions new apigateway.StepFunctionsRestApi(this, 'StepFunctionsRestApi', { - stateMachine: stateMachine, + stateMachine: machine, headers: true, path: false, querystring: false, requestContext: { - caller: true, - user: true, + caller: true, + user: true, }, }); ``` diff --git a/packages/@aws-cdk/aws-apigateway/rosetta/stepfunctions.ts-fixture b/packages/@aws-cdk/aws-apigateway/rosetta/stepfunctions.ts-fixture new file mode 100644 index 0000000000000..eb7728585bcde --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/rosetta/stepfunctions.ts-fixture @@ -0,0 +1,17 @@ +import { Construct } from 'constructs'; +import { Stack } from '@aws-cdk/core'; +import apigateway = require('@aws-cdk/aws-apigateway'); +import stepfunctions = require('@aws-cdk/aws-stepfunctions'); + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const machine: stepfunctions.IStateMachine = new stepfunctions.StateMachine(this, 'StateMachine', { + definition: new stepfunctions.Pass(this, 'PassState'), + stateMachineType: stepfunctions.StateMachineType.EXPRESS, + }); + + /// here + } +} From 2623b656fcf62dfdb0675d3f5f5c38c12fdb631b Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Thu, 25 Nov 2021 11:48:28 +0000 Subject: [PATCH 34/35] code tweaks --- packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts | 4 +--- .../aws-apigateway/test/integrations/stepfunctions.test.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts b/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts index 52a3d1513f877..69addc0619383 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts @@ -101,9 +101,7 @@ export class StepFunctionsRestApi extends RestApi { super(scope, id, props); this.root.addMethod('ANY', stepfunctionsIntegration, { - methodResponses: [ - ...methodResponse(), - ], + methodResponses: methodResponse(), }); } } diff --git a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts index 24169ee721093..c803fa974f7f6 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts @@ -5,7 +5,7 @@ import { StateMachine, StateMachineType } from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; import * as apigw from '../../lib'; -describe('StepFunctionsExecutionIntegration', () => { +describe('StepFunctionsIntegration', () => { describe('startExecution', () => { test('minimal setup', () => { //GIVEN From 57763b8a75da69a401cf042756de33a4a4be0385 Mon Sep 17 00:00:00 2001 From: Diego Santiviago Date: Thu, 25 Nov 2021 06:36:57 -0800 Subject: [PATCH 35/35] Remove stringLike fix as it is merged from the other PR --- .../assert-internal/lib/assertions/have-resource-matchers.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/@aws-cdk/assert-internal/lib/assertions/have-resource-matchers.ts b/packages/@aws-cdk/assert-internal/lib/assertions/have-resource-matchers.ts index 7862542da5480..3671c051e78e8 100644 --- a/packages/@aws-cdk/assert-internal/lib/assertions/have-resource-matchers.ts +++ b/packages/@aws-cdk/assert-internal/lib/assertions/have-resource-matchers.ts @@ -243,8 +243,6 @@ function isCallable(x: any): x is ((...args: any[]) => any) { */ export function stringLike(pattern: string): PropertyMatcher { // Replace * with .* in the string, escape the rest and brace with ^...$ - - // needs rebase from https://github.com/aws/aws-cdk/pull/17692/files const regex = new RegExp(`^${pattern.split('*').map(escapeRegex).join('.*')}$`, 'm'); return annotateMatcher({ $stringContaining: pattern }, (value: any, failure: InspectionFailure) => {