From 2015d588daa04c7f0bd615a297c5e01d247c4ff8 Mon Sep 17 00:00:00 2001 From: Saqib Dhuka Date: Fri, 1 Oct 2021 21:19:58 +0000 Subject: [PATCH] Added StepFunctionsStartExecutionIntegration and StepFunctionsStartSyncExecutionIntegration. Added unit and integration tests. Fixed issues mentioned in the Pull-request. Added example to README closes #11947. --- .github/ISSUE_TEMPLATE.md | 34 +++ .github/ISSUE_TEMPLATE/bug.md | 54 +++++ .github/ISSUE_TEMPLATE/doc.md | 29 +++ .github/ISSUE_TEMPLATE/feature-request.md | 46 ++++ .github/ISSUE_TEMPLATE/general-issues.md | 35 +++ .github/ISSUE_TEMPLATE/tracking.md | 68 ++++++ .../aws-apigatewayv2-integrations/README.md | 46 ++++ .../lib/http/aws.ts | 168 +++++++++++++ .../lib/http/base-types.ts | 21 ++ .../lib/http/index.ts | 1 + .../lib/http/private/integration.ts | 64 +++++ .../package.json | 7 +- .../test/http/aws.test.ts | 95 ++++++++ ...s-integration-startExecution.expected.json | 196 +++++++++++++++ .../integ.aws-integration-startExecution.ts | 29 +++ ...tegration-startSyncExecution.expected.json | 227 ++++++++++++++++++ ...nteg.aws-integration-startSyncExecution.ts | 29 +++ .../@aws-cdk/aws-apigatewayv2/lib/http/api.ts | 5 + .../aws-apigatewayv2/lib/http/integration.ts | 170 +++++++++---- .../api/util/cloudformation/evaluate-cfn.ts | 89 +++++++ 20 files changed, 1369 insertions(+), 44 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/ISSUE_TEMPLATE/bug.md create mode 100644 .github/ISSUE_TEMPLATE/doc.md create mode 100644 .github/ISSUE_TEMPLATE/feature-request.md create mode 100644 .github/ISSUE_TEMPLATE/general-issues.md create mode 100644 .github/ISSUE_TEMPLATE/tracking.md create mode 100644 packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/aws.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/aws.test.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.aws-integration-startExecution.expected.json create mode 100644 packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.aws-integration-startExecution.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.aws-integration-startSyncExecution.expected.json create mode 100644 packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.aws-integration-startSyncExecution.ts create mode 100644 packages/aws-cdk/lib/api/util/cloudformation/evaluate-cfn.ts diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000000..5371d422793e3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,34 @@ +--- +name: "\U00002753 General Issue" +about: Create a new issue +labels: needs-triage +--- + + + +## :question: General Issue + + + +### The Question + + +### Environment + + - **CDK CLI Version:** + - **Module Version:** + - **Node.js Version:** + - **OS:** + - **Language:** + + +### Other information + diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 0000000000000..6835abe99e034 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,54 @@ +--- +name: "\U0001F41B Bug Report" +about: Report a bug +title: "(module name): short issue description" +labels: bug, needs-triage +--- + + + + + + +### Reproduction Steps + + + +### What did you expect to happen? + + + +### What actually happened? + + + + +### Environment + + - **CDK CLI Version :** + - **Framework Version:** + - **Node.js Version:** + - **OS :** + - **Language (Version):** + +### Other + + + + + + +--- + +This is :bug: Bug Report diff --git a/.github/ISSUE_TEMPLATE/doc.md b/.github/ISSUE_TEMPLATE/doc.md new file mode 100644 index 0000000000000..3c8a1dc691d0e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/doc.md @@ -0,0 +1,29 @@ +--- +name: "đź“• Documentation Issue" +about: Issue in the reference documentation or developer guide +title: "(module name): short issue description" +labels: feature-request, documentation, needs-triage +--- + + + + + + + + + + + + + +--- + +This is a đź“• documentation issue diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 0000000000000..163f2f54d0b88 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,46 @@ +--- +name: "\U0001F680 Feature Request" +about: Request a new feature +title: "(module name): short issue description" +labels: feature-request, needs-triage +--- + + + + + + + +### Use Case + + + + + + + +### Proposed Solution + + + + + + + +### Other + + + + + + + +* [ ] :wave: I may be able to implement this feature request +* [ ] :warning: This feature might incur a breaking change + +--- + +This is a :rocket: Feature Request diff --git a/.github/ISSUE_TEMPLATE/general-issues.md b/.github/ISSUE_TEMPLATE/general-issues.md new file mode 100644 index 0000000000000..2b478904a6fca --- /dev/null +++ b/.github/ISSUE_TEMPLATE/general-issues.md @@ -0,0 +1,35 @@ +--- +name: "\U00002753 General Issue" +about: Create a new issue +title: "(module name): short issue description" +labels: needs-triage, guidance +--- + + + +## :question: General Issue + + + +### The Question + + +### Environment + + - **CDK CLI Version:** + - **Module Version:** + - **Node.js Version:** + - **OS:** + - **Language (Version):** + + +### Other information + diff --git a/.github/ISSUE_TEMPLATE/tracking.md b/.github/ISSUE_TEMPLATE/tracking.md new file mode 100644 index 0000000000000..b3655dfaa6dca --- /dev/null +++ b/.github/ISSUE_TEMPLATE/tracking.md @@ -0,0 +1,68 @@ +--- +name: "đź“Š Tracking Issue" +title: "đź“ŠTracking: [service]" +about: Add a module tracking issue (internal use only) +labels: management/tracking +--- + +Add your +1 đź‘Ť to help us prioritize high-level constructs for this service +--- + +### Overview: + + + + + + + +[AWS Docs](url) + +### Maturity: CloudFormation Resources Only + + +See the [AWS Construct Library Module Lifecycle doc](https://github.com/aws/aws-cdk-rfcs/blob/master/text/0107-construct-library-module-lifecycle.md) for more information about maturity levels. + + +### Implementation: + +See the [CDK API Reference](url) for more implementation details. + + + + + + +### Issue list: + + + + + + + + +--- +This is a đź“ŠTracking Issue diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md b/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md index cce77fd6398e6..8a366780d8a68 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md @@ -20,6 +20,7 @@ - [HTTP APIs](#http-apis) - [Lambda Integration](#lambda) - [HTTP Proxy Integration](#http-proxy) + - [AWS Service Integration](#aws-service) - [Private Integration](#private-integration) - [WebSocket APIs](#websocket-apis) - [Lambda WebSocket Integration](#lambda-websocket-integration) @@ -78,6 +79,51 @@ httpApi.addRoutes({ }); ``` +### AWS Service + +You can integrate your HTTP API with AWS services by using first-class integrations. A first-class integration connects +an HTTP API route to an AWS service API. When a client invokes a route that's backed by a first-class integration, +API Gateway invokes an AWS service API for you. More information can be found at [Working with AWS service integrations for HTTP APIs] +(https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-aws-services.html). + +The following code configures a route `POST /start` to start execution of Standard Step Functions state machine. + +```ts +const httpApi = new HttpApi(stack, 'HttpApi'); +const state = new StateMachine(stack, 'MyStateMachine', { + definition: Chain.start(new Pass(stack, 'Pass')), + stateMachineType: StateMachineType.STANDARD, +}); +httpApi.addRoutes({ + path: '/start', + methods: [ HttpMethod.POST ], + integration: new StepFunctionsStartExecutionIntegration({ + stateMachine: state, + input: '$request.body', + timeout: Duration.seconds(10), + }), +}); +``` + +The following code configures a route `POST /start` to start synchronous execution of Express Step Functions state machine. + +```ts +const httpApi = new HttpApi(stack, 'HttpApi'); +const state = new StateMachine(stack, 'MyStateMachine', { + definition: Chain.start(new Pass(stack, 'Pass')), + stateMachineType: StateMachineType.EXPRESS, +}); +httpApi.addRoutes({ + path: '/start', + methods: [ HttpMethod.POST ], + integration: new StepFunctionsStartSyncExecutionIntegration({ + stateMachine: state, + input: '$request.body', + timeout: Duration.seconds(10), + }), +}); +``` + ### Private Integration Private integrations enable integrating an HTTP API route with private resources in a VPC, such as Application Load Balancers or diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/aws.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/aws.ts new file mode 100644 index 0000000000000..9ef0a6a1bb1c2 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/aws.ts @@ -0,0 +1,168 @@ +import * as iam from '@aws-cdk/aws-iam'; +import { IStateMachine } from '@aws-cdk/aws-stepfunctions'; +import { Construct } from 'constructs'; +import { AwsServiceIntegration, AwsServiceIntegrationProps } from './private/integration'; + +/** + * The common Step Functions integration resource for HTTP API + */ +abstract class StepFunctionsIntegration extends AwsServiceIntegration { + + /** + * + * @internal + */ + protected _integrationService(): string { + return 'StepFunctions'; + } +} + +/** + * Step Functions StartExecution integration properties + */ +export interface StepFunctionsStartExecutionIntegrationProps extends AwsServiceIntegrationProps { + + /** + * The state machine to be executed + */ + readonly stateMachine: IStateMachine; + + /** + * The execution name + * + * @default - undefined + */ + readonly name?: string; + + /** + * The input parameters of execution + * + * @default - undefined + */ + readonly input?: any; + + /** + * The region of state machine + * + * @default - undefined + */ + readonly region?: string; + + /** + * Passes the AWS X-Ray trace header. The trace header can also be passed in the request payload. + * + * @default - undefined + */ + readonly traceHeader?: string; +} + +/** + * The StepFunctions-StartExecution integration resource for HTTP API + */ +export class StepFunctionsStartExecutionIntegration extends StepFunctionsIntegration { + + constructor(private readonly _scope: Construct, private readonly _props: StepFunctionsStartExecutionIntegrationProps) { + super(_props); + } + + /** + * + * @internal + */ + protected _integrationAction(): string { + return 'StartExecution'; + } + + /** + * + * @internal + */ + protected _fulfillRole(credentialsRole: iam.IRole): void { + this._props.stateMachine.grantStartExecution(credentialsRole); + credentialsRole.attachInlinePolicy( + new iam.Policy(this._scope, 'AllowSfnExec', { + statements: [ + new iam.PolicyStatement({ + actions: ['states:StartExecution'], + effect: iam.Effect.ALLOW, + resources: [this._props.stateMachine.stateMachineArn], + }), + ], + }), + ); + + } + + /** + * + * @internal + */ + protected _buildRequestParameters(): { [key: string]: any } { + return { + StateMachineArn: this._props.stateMachine.stateMachineArn, + Name: this._props.name, + Input: this._props.input, + Region: this._props.region, + TraceHeader: this._props.traceHeader, + }; + } +} + +/** + * Step Functions StartSyncExecution integration properties + */ +export interface StepFunctionsStartSyncExecutionIntegrationProps extends StepFunctionsStartExecutionIntegrationProps { +} + +/** + * The StepFunctions-StartExecution integration resource for HTTP API + */ +export class StepFunctionsStartSyncExecutionIntegration extends StepFunctionsIntegration { + + constructor(private readonly _scope: Construct, private readonly _props: StepFunctionsStartSyncExecutionIntegrationProps) { + super(_props); + } + + /** + * + * @internal + */ + protected _integrationAction(): string { + return 'StartSyncExecution'; + } + + /** + * + * @internal + */ + protected _fulfillRole(credentialsRole: iam.IRole): void { + + this._props.stateMachine.grantExecution(credentialsRole.grantPrincipal, 'states:StartSyncExecution'); + credentialsRole.attachInlinePolicy( + new iam.Policy(this._scope, 'AllowSfnSyncExec', { + statements: [ + new iam.PolicyStatement({ + actions: ['states:StartSyncExecution'], + effect: iam.Effect.ALLOW, + resources: [this._props.stateMachine.stateMachineArn], + }), + ], + }), + ); + + } + + /** + * + * @internal + */ + protected _buildRequestParameters(): { [key: string]: any } { + return { + StateMachineArn: this._props.stateMachine.stateMachineArn, + Name: this._props.name, + Input: this._props.input, + Region: this._props.region, + TraceHeader: this._props.traceHeader, + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/base-types.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/base-types.ts index db14e50f7fc54..a87f71c3ddd46 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/base-types.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/base-types.ts @@ -1,4 +1,5 @@ import { HttpMethod, IVpcLink } from '@aws-cdk/aws-apigatewayv2'; +import { Duration } from '@aws-cdk/core'; /** * Base options for private integration @@ -25,3 +26,23 @@ export interface HttpPrivateIntegrationOptions { readonly secureServerName?: string; } + +/** + * Common properties to initialize a new `HttpProxyIntegration`. + */ +export interface CommonIntegrationProps { + + /** + * The description of the integration + * + * @default - undefined + */ + readonly description?: string; + + /** + * Custom timeout for HTTP APIs + * + * @default - undefined + */ + readonly timeout?: Duration; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/index.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/index.ts index 8e0598975f8cb..9bb5a46315f63 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/index.ts @@ -4,3 +4,4 @@ export * from './nlb'; export * from './service-discovery'; export * from './http-proxy'; export * from './lambda'; +export * from './aws'; diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/private/integration.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/private/integration.ts index 6d32b22794722..62e8ae75f9c6e 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/private/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/private/integration.ts @@ -9,6 +9,8 @@ import { IVpcLink, } from '@aws-cdk/aws-apigatewayv2'; import * as ec2 from '@aws-cdk/aws-ec2'; +import { IRole, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; +import { CommonIntegrationProps } from '../base-types'; /** @@ -63,3 +65,65 @@ export abstract class HttpPrivateIntegration implements IHttpRouteIntegration { public abstract bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig; } + +/** + * Aws Service integration properties + * + * @internal + */ +export interface AwsServiceIntegrationProps extends CommonIntegrationProps { +} + +/** + * The Aws Service integration resource for HTTP API + * + * @internal + */ +export abstract class AwsServiceIntegration implements IHttpRouteIntegration { + + constructor(private readonly props: AwsServiceIntegrationProps) { + } + + /** + * + * @internal + */ + protected abstract _fulfillRole(credentialsRole: IRole): void; + + /** + * + * @internal + */ + protected abstract _buildRequestParameters(): { [key: string]: any }; + + /** + * + * @internal + */ + protected abstract _integrationService(): string; + + /** + * + * @internal + */ + protected abstract _integrationAction(): string; + + public bind(_options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { + + const apiGatewayIntegrationRole = new Role(_options.scope, 'ApiGatewayIntegrationRole', { + assumedBy: new ServicePrincipal('apigateway.amazonaws.com'), + }); + this._fulfillRole(apiGatewayIntegrationRole); + + return { + payloadFormatVersion: PayloadFormatVersion.VERSION_1_0, // 1.0 is required and is the only supported format + type: HttpIntegrationType.LAMBDA_PROXY, + subtype: `${this._integrationService()}-${this._integrationAction()}`, + credentials: apiGatewayIntegrationRole.roleArn, + timeout: this.props.timeout, + description: this.props.description, + requestParameters: this._buildRequestParameters(), + }; + } + +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json index a950d542429e0..fdb05b46bf601 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json @@ -73,8 +73,11 @@ "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", + "@aws-cdk/assert-internal": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^26.0.24" + "@types/jest": "^26.0.24", + "@types/nodeunit": "^0.0.31", + "nodeunit": "^0.11.3" }, "dependencies": { "@aws-cdk/aws-apigatewayv2": "0.0.0", @@ -83,6 +86,7 @@ "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-servicediscovery": "0.0.0", + "@aws-cdk/aws-stepfunctions": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, @@ -93,6 +97,7 @@ "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-servicediscovery": "0.0.0", + "@aws-cdk/aws-stepfunctions": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/aws.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/aws.test.ts new file mode 100644 index 0000000000000..6a602f540d9ed --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/aws.test.ts @@ -0,0 +1,95 @@ +import '@aws-cdk/assert-internal/jest'; +import { HttpApi, HttpRoute, HttpRouteKey } from '@aws-cdk/aws-apigatewayv2'; +import { StateMachine, Chain, Pass, StateMachineType } from '@aws-cdk/aws-stepfunctions'; +import { Stack, Duration } from '@aws-cdk/core'; +import { StepFunctionsStartExecutionIntegration, StepFunctionsStartSyncExecutionIntegration } from '../../lib'; + +describe('AwsServiceIntegration', () => { + test('StepFunctions-StartExecution', () => { + const stack = new Stack(); + const api = new HttpApi(stack, 'HttpApi'); + + new HttpRoute(stack, 'StepFunctionsStartExeRoute', { + httpApi: api, + integration: new StepFunctionsStartExecutionIntegration(stack, { + stateMachine: stateMachine(stack), + name: 'MyExe', + input: '$request.body', + timeout: Duration.seconds(10), + description: 'Start execution of state machine', + }), + routeKey: HttpRouteKey.with('/start'), + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + ApiId: { + Ref: 'HttpApiF5A9A8A7', + }, + IntegrationType: 'AWS_PROXY', + CredentialsArn: { + 'Fn::GetAtt': [ + 'StepFunctionsStartExeRouteApiGatewayIntegrationRoleE88875A9', + 'Arn', + ], + }, + Description: 'Start execution of state machine', + IntegrationSubtype: 'StepFunctions-StartExecution', + PayloadFormatVersion: '1.0', + RequestParameters: { + StateMachineArn: { + Ref: 'MyStateMachine6C968CA5', + }, + Input: '$request.body', + Name: 'MyExe', + }, + TimeoutInMillis: 10000, + }); + }); + + test('StepFunctions-StartSyncExecution', () => { + const stack = new Stack(); + const api = new HttpApi(stack, 'HttpApi'); + + new HttpRoute(stack, 'StepFunctionsStartSyncExeRoute', { + httpApi: api, + integration: new StepFunctionsStartSyncExecutionIntegration(stack, { + stateMachine: new StateMachine(stack, 'MyStateMachine', { + stateMachineName: 'MyStateMachine', + definition: Chain.start(new Pass(stack, 'Pass')), + stateMachineType: StateMachineType.EXPRESS, + }), + input: '$request.body', + }), + routeKey: HttpRouteKey.with('/startSync'), + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + ApiId: { + Ref: 'HttpApiF5A9A8A7', + }, + IntegrationType: 'AWS_PROXY', + CredentialsArn: { + 'Fn::GetAtt': [ + 'StepFunctionsStartSyncExeRouteApiGatewayIntegrationRole67241EA0', + 'Arn', + ], + }, + IntegrationSubtype: 'StepFunctions-StartSyncExecution', + PayloadFormatVersion: '1.0', + RequestParameters: { + StateMachineArn: { + Ref: 'MyStateMachine6C968CA5', + }, + Input: '$request.body', + }, + }); + }); +}); + +function stateMachine(stack: Stack): StateMachine { + return new StateMachine(stack, 'MyStateMachine', { + stateMachineName: 'MyStateMachine', + definition: Chain.start(new Pass(stack, 'Pass')), + stateMachineType: StateMachineType.STANDARD, + }); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.aws-integration-startExecution.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.aws-integration-startExecution.expected.json new file mode 100644 index 0000000000000..9db2e8916d4c7 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.aws-integration-startExecution.expected.json @@ -0,0 +1,196 @@ +{ + "Resources": { + "MyStateMachineRoleD59FFEBC": { + "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" + } + } + }, + "MyStateMachine6C968CA5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "MyStateMachineRoleD59FFEBC", + "Arn" + ] + }, + "DefinitionString": "{\"StartAt\":\"Pass\",\"States\":{\"Pass\":{\"Type\":\"Pass\",\"End\":true}}}", + "StateMachineType": "STANDARD" + }, + "DependsOn": [ + "MyStateMachineRoleD59FFEBC" + ] + }, + "AwsIntegrationApi1AEE0491": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "AwsIntegrationApi", + "ProtocolType": "HTTP" + } + }, + "AwsIntegrationApiDefaultRouteApiGatewayIntegrationRole57718B44": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "AwsIntegrationApiDefaultRouteApiGatewayIntegrationRoleDefaultPolicy21C90B92": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "MyStateMachine6C968CA5" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AwsIntegrationApiDefaultRouteApiGatewayIntegrationRoleDefaultPolicy21C90B92", + "Roles": [ + { + "Ref": "AwsIntegrationApiDefaultRouteApiGatewayIntegrationRole57718B44" + } + ] + } + }, + "AwsIntegrationApiDefaultRouteHttpIntegration2f0cb9144d46bb679d933c010c9875266F1CBFB8": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "AwsIntegrationApi1AEE0491" + }, + "IntegrationType": "AWS_PROXY", + "CredentialsArn": { + "Fn::GetAtt": [ + "AwsIntegrationApiDefaultRouteApiGatewayIntegrationRole57718B44", + "Arn" + ] + }, + "IntegrationSubtype": "StepFunctions-StartExecution", + "PayloadFormatVersion": "1.0", + "RequestParameters": { + "StateMachineArn": { + "Ref": "MyStateMachine6C968CA5" + }, + "Input": "$request.body" + } + } + }, + "AwsIntegrationApiDefaultRouteF019925B": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "AwsIntegrationApi1AEE0491" + }, + "RouteKey": "$default", + "AuthorizationType": "NONE", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "AwsIntegrationApiDefaultRouteHttpIntegration2f0cb9144d46bb679d933c010c9875266F1CBFB8" + } + ] + ] + } + } + }, + "AwsIntegrationApiDefaultStageDFAEF224": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "AwsIntegrationApi1AEE0491" + }, + "StageName": "$default", + "AutoDeploy": true + } + }, + "AllowSfnExecFA6BC8D6": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "MyStateMachine6C968CA5" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AllowSfnExecFA6BC8D6", + "Roles": [ + { + "Ref": "AwsIntegrationApiDefaultRouteApiGatewayIntegrationRole57718B44" + } + ] + } + } + }, + "Outputs": { + "Endpoint": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "AwsIntegrationApi1AEE0491" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.aws-integration-startExecution.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.aws-integration-startExecution.ts new file mode 100644 index 0000000000000..9dbd7ac1c94c3 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.aws-integration-startExecution.ts @@ -0,0 +1,29 @@ +import { HttpApi } from '@aws-cdk/aws-apigatewayv2'; +import { StateMachine, Chain, StateMachineType, Pass } from '@aws-cdk/aws-stepfunctions'; +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { StepFunctionsStartExecutionIntegration } from '../../lib'; + +/* + * Stack verification steps: + * "curl " should return 'success' + */ + +const app = new App(); + +const stack = new Stack(app, 'integ-aws-service'); + +const state = new StateMachine(stack, 'MyStateMachine', { + definition: Chain.start(new Pass(stack, 'Pass')), + stateMachineType: StateMachineType.STANDARD, +}); + +const endpoint = new HttpApi(stack, 'AwsIntegrationApi', { + defaultIntegration: new StepFunctionsStartExecutionIntegration(stack, { + stateMachine: state, + input: '$request.body', + }), +}); + +new CfnOutput(stack, 'Endpoint', { + value: endpoint.url!, +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.aws-integration-startSyncExecution.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.aws-integration-startSyncExecution.expected.json new file mode 100644 index 0000000000000..d682a8765061a --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.aws-integration-startSyncExecution.expected.json @@ -0,0 +1,227 @@ +{ + "Resources": { + "MyStateMachineRoleD59FFEBC": { + "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" + } + } + }, + "MyStateMachine6C968CA5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "MyStateMachineRoleD59FFEBC", + "Arn" + ] + }, + "DefinitionString": "{\"StartAt\":\"Pass\",\"States\":{\"Pass\":{\"Type\":\"Pass\",\"End\":true}}}", + "StateMachineType": "EXPRESS" + }, + "DependsOn": [ + "MyStateMachineRoleD59FFEBC" + ] + }, + "AwsIntegrationApi1AEE0491": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "AwsIntegrationApi", + "ProtocolType": "HTTP" + } + }, + "AwsIntegrationApiDefaultRouteApiGatewayIntegrationRole57718B44": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "AwsIntegrationApiDefaultRouteApiGatewayIntegrationRoleDefaultPolicy21C90B92": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartSyncExecution", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":states:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":execution:", + { + "Fn::Select": [ + 6, + { + "Fn::Split": [ + ":", + { + "Ref": "MyStateMachine6C968CA5" + } + ] + } + ] + }, + ":*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AwsIntegrationApiDefaultRouteApiGatewayIntegrationRoleDefaultPolicy21C90B92", + "Roles": [ + { + "Ref": "AwsIntegrationApiDefaultRouteApiGatewayIntegrationRole57718B44" + } + ] + } + }, + "AwsIntegrationApiDefaultRouteHttpIntegration4c730d5cbb3beb8802425fa8380c19355D85A87E": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "AwsIntegrationApi1AEE0491" + }, + "IntegrationType": "AWS_PROXY", + "CredentialsArn": { + "Fn::GetAtt": [ + "AwsIntegrationApiDefaultRouteApiGatewayIntegrationRole57718B44", + "Arn" + ] + }, + "IntegrationSubtype": "StepFunctions-StartSyncExecution", + "PayloadFormatVersion": "1.0", + "RequestParameters": { + "StateMachineArn": { + "Ref": "MyStateMachine6C968CA5" + }, + "Input": "$request.body" + } + } + }, + "AwsIntegrationApiDefaultRouteF019925B": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "AwsIntegrationApi1AEE0491" + }, + "RouteKey": "$default", + "AuthorizationType": "NONE", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "AwsIntegrationApiDefaultRouteHttpIntegration4c730d5cbb3beb8802425fa8380c19355D85A87E" + } + ] + ] + } + } + }, + "AwsIntegrationApiDefaultStageDFAEF224": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "AwsIntegrationApi1AEE0491" + }, + "StageName": "$default", + "AutoDeploy": true + } + }, + "AllowSfnSyncExec72CF68FA": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartSyncExecution", + "Effect": "Allow", + "Resource": { + "Ref": "MyStateMachine6C968CA5" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AllowSfnSyncExec72CF68FA", + "Roles": [ + { + "Ref": "AwsIntegrationApiDefaultRouteApiGatewayIntegrationRole57718B44" + } + ] + } + } + }, + "Outputs": { + "Endpoint": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "AwsIntegrationApi1AEE0491" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.aws-integration-startSyncExecution.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.aws-integration-startSyncExecution.ts new file mode 100644 index 0000000000000..5200bcc8ce94f --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.aws-integration-startSyncExecution.ts @@ -0,0 +1,29 @@ +import { HttpApi } from '@aws-cdk/aws-apigatewayv2'; +import { StateMachine, Chain, StateMachineType, Pass } from '@aws-cdk/aws-stepfunctions'; +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { StepFunctionsStartSyncExecutionIntegration } from '../../lib'; + +/* + * Stack verification steps: + * "curl " should return 'success' + */ + +const app = new App(); + +const stack = new Stack(app, 'integ-aws-service'); + +const state = new StateMachine(stack, 'MyStateMachine', { + definition: Chain.start(new Pass(stack, 'Pass')), + stateMachineType: StateMachineType.EXPRESS, +}); + +const endpoint = new HttpApi(stack, 'AwsIntegrationApi', { + defaultIntegration: new StepFunctionsStartSyncExecutionIntegration(stack, { + stateMachine: state, + input: '$request.body', + }), +}); + +new CfnOutput(stack, 'Endpoint', { + value: endpoint.url!, +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts index 254a29ea6d28b..c854a2da7ec0e 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts @@ -298,12 +298,17 @@ abstract class HttpApiBase extends ApiBase implements IHttpApi { // note that th const integration = new HttpIntegration(scope, `HttpIntegration-${configHash}`, { httpApi: this, + description: config.description, integrationType: config.type, + integrationSubtype: config.subtype, integrationUri: config.uri, + requestParameters: config.requestParameters, + credentials: config.credentials, method: config.method, connectionId: config.connectionId, connectionType: config.connectionType, payloadFormatVersion: config.payloadFormatVersion, + timeout: config.timeout, secureServerName: config.secureServerName, }); this._integrationCache.saveIntegration(scope, config, integration); diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts index f832b5b7e3b21..d9b05c0e70e3a 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts @@ -1,5 +1,5 @@ /* eslint-disable quotes */ -import { Resource } from '@aws-cdk/core'; +import { Resource, Duration } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnIntegration } from '../apigatewayv2.generated'; import { IIntegration } from '../common'; @@ -84,42 +84,79 @@ export interface HttpIntegrationProps { readonly httpApi: IHttpApi; /** - * Integration type - */ + * The description of the integration + * + * @default - undefined + */ + readonly description?: string; + + /** + * Specifies the credentials ARN required for the integration, if any. + * + * @default - undefined + */ + readonly credentials?: string; + + /** + * Integration type + */ readonly integrationType: HttpIntegrationType; /** - * Integration URI. - * This will be the function ARN in the case of `HttpIntegrationType.LAMBDA_PROXY`, - * or HTTP URL in the case of `HttpIntegrationType.HTTP_PROXY`. - */ - readonly integrationUri: string; + * Specifies the AWS service action to invoke. + * + * @default - undefined + */ + readonly integrationSubtype?: string; /** - * The HTTP method to use when calling the underlying HTTP proxy - * @default - none. required if the integration type is `HttpIntegrationType.HTTP_PROXY`. - */ + * Custom timeout for HTTP APIs + * + * @default - undefined + */ + readonly timeout?: Duration; + + /** + * Request parameters are a key-value map specifying parameters that are passed to AWS_PROXY integrations. + * + * @default - undefined + */ + readonly requestParameters?: { [key: string]: any }; + + /** + * Integration URI. + * This will be the function ARN in the case of `HttpIntegrationType.LAMBDA_PROXY`, + * or HTTP URL in the case of `HttpIntegrationType.HTTP_PROXY`. + * + * @default - undefined + */ + readonly integrationUri?: string; + + /** + * The HTTP method to use when calling the underlying HTTP proxy + * @default - none. required if the integration type is `HttpIntegrationType.HTTP_PROXY`. + */ readonly method?: HttpMethod; /** - * The ID of the VPC link for a private integration. Supported only for HTTP APIs. - * - * @default - undefined - */ + * The ID of the VPC link for a private integration. Supported only for HTTP APIs. + * + * @default - undefined + */ readonly connectionId?: string; /** - * The type of the network connection to the integration endpoint - * - * @default HttpConnectionType.INTERNET - */ + * The type of the network connection to the integration endpoint + * + * @default HttpConnectionType.INTERNET + */ readonly connectionType?: HttpConnectionType; /** - * The version of the payload format - * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html - * @default - defaults to latest in the case of HttpIntegrationType.LAMBDA_PROXY`, irrelevant otherwise. - */ + * The version of the payload format + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html + * @default - defaults to latest in the case of HttpIntegrationType.LAMBDA_PROXY`, irrelevant otherwise. + */ readonly payloadFormatVersion?: PayloadFormatVersion; /** @@ -141,14 +178,24 @@ export class HttpIntegration extends Resource implements IHttpIntegration { constructor(scope: Construct, id: string, props: HttpIntegrationProps) { super(scope, id); + + if (props.timeout && (props.timeout.toMilliseconds() < 50 || props.timeout.toMilliseconds() > 30000)) { + throw new Error(`The timeout of HTTP integration should be between 50 and 30,000, got ${props.timeout.toMilliseconds()}.`); + } + const integ = new CfnIntegration(this, 'Resource', { apiId: props.httpApi.apiId, + description: props.description, integrationType: props.integrationType, + integrationSubtype: props.integrationSubtype, integrationUri: props.integrationUri, integrationMethod: props.method, + credentialsArn: props.credentials, + requestParameters: props.requestParameters, connectionId: props.connectionId, connectionType: props.connectionType, payloadFormatVersion: props.payloadFormatVersion?.version, + timeoutInMillis: props.timeout?.toMilliseconds(), }); if (props.secureServerName) { @@ -194,41 +241,78 @@ export interface IHttpRouteIntegration { */ export interface HttpRouteIntegrationConfig { /** - * Integration type. + * The description of the integration + * + * @default - undefined */ + readonly description?: string; + + /** + * Integration type. + */ readonly type: HttpIntegrationType; /** - * Integration URI - */ - readonly uri: string; + * Specifies the credentials ARN required for the integration, if any. + * + * @default - undefined + */ + readonly credentials?: string; /** - * The HTTP method that must be used to invoke the underlying proxy. - * Required for `HttpIntegrationType.HTTP_PROXY` - * @default - undefined - */ + * Specifies the AWS service action to invoke. + * + * @default - undefined + */ + readonly subtype?: string; + + /** + * Custom timeout for HTTP APIs + * + * @default - undefined + */ + readonly timeout?: Duration; + + /** + * Request parameters are a key-value map specifying parameters that are passed to AWS_PROXY integrations. + * + * @default - undefined + */ + readonly requestParameters?: { [key: string]: any }; + + /** + * Integration URI + * + * @default - undefined + */ + readonly uri?: string; + + /** + * The HTTP method that must be used to invoke the underlying proxy. + * Required for `HttpIntegrationType.HTTP_PROXY` + * @default - undefined + */ readonly method?: HttpMethod; /** - * The ID of the VPC link for a private integration. Supported only for HTTP APIs. - * - * @default - undefined - */ + * The ID of the VPC link for a private integration. Supported only for HTTP APIs. + * + * @default - undefined + */ readonly connectionId?: string; /** - * The type of the network connection to the integration endpoint - * - * @default HttpConnectionType.INTERNET - */ + * The type of the network connection to the integration endpoint + * + * @default HttpConnectionType.INTERNET + */ readonly connectionType?: HttpConnectionType; /** - * Payload format version in the case of lambda proxy integration - * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html - * @default - undefined - */ + * Payload format version in the case of lambda proxy integration + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html + * @default - undefined + */ readonly payloadFormatVersion: PayloadFormatVersion; /** diff --git a/packages/aws-cdk/lib/api/util/cloudformation/evaluate-cfn.ts b/packages/aws-cdk/lib/api/util/cloudformation/evaluate-cfn.ts new file mode 100644 index 0000000000000..bdc395df83814 --- /dev/null +++ b/packages/aws-cdk/lib/api/util/cloudformation/evaluate-cfn.ts @@ -0,0 +1,89 @@ +export function evaluateCfn(object: any, context: { [key: string]: string }): any { + const intrinsicFns: any = { + 'Fn::Join'(separator: string, args: string[]): string { + return evaluate(args).map(evaluate).join(separator); + }, + + 'Fn::Split'(separator: string, args: string): string { + return evaluate(args).split(separator); + }, + + 'Fn::Select'(index: number, args: string[]): string { + return evaluate(args).map(evaluate)[index]; + }, + + 'Ref'(logicalId: string): string { + if (logicalId in context) { + return context[logicalId]; + } else { + throw new Error(`Reference target '${logicalId}' was not found`); + } + }, + + 'Fn::Sub'(template: string, explicitPlaceholders?: { [variable: string]: string }): string { + const placeholders = explicitPlaceholders + ? { ...context, ...evaluate(explicitPlaceholders) } + : context; + + return template.replace(/\${([^}]*)}/g, (_: string, key: string) => { + if (key in placeholders) { + return placeholders[key]; + } else { + throw new Error(`Fn::Sub target '${key}' was not found`); + } + }); + }, + }; + + return evaluate(object); + + function evaluate(obj: any): any { + if (Array.isArray(obj)) { + return obj.map(evaluate); + } + + if (typeof obj === 'object') { + const intrinsic = parseIntrinsic(obj); + if (intrinsic) { + return evaluateIntrinsic(intrinsic); + } + + const ret: { [key: string]: any } = {}; + for (const key of Object.keys(obj)) { + ret[key] = evaluate(obj[key]); + } + return ret; + } + + return obj; + } + + function evaluateIntrinsic(intrinsic: Intrinsic) { + if (!(intrinsic.name in intrinsicFns)) { + throw new Error(`Intrinsic ${intrinsic.name} not supported here`); + } + + const argsAsArray = Array.isArray(intrinsic.args) ? intrinsic.args : [intrinsic.args]; + + return intrinsicFns[intrinsic.name].apply(intrinsicFns, argsAsArray); + } +} + +interface Intrinsic { + readonly name: string; + readonly args: any; +} + +function parseIntrinsic(x: any): Intrinsic | undefined { + if (typeof x !== 'object' || x === null) { + return undefined; + } + const keys = Object.keys(x); + if (keys.length === 1 && (keys[0].startsWith('Fn::') || keys[0] === 'Ref')) { + return { + name: keys[0], + args: x[keys[0]], + }; + } + return undefined; +}