From cc608d055ffefb798ad6378ab07f36cb241897da Mon Sep 17 00:00:00 2001 From: Ayush Goyal Date: Thu, 11 Mar 2021 00:08:21 +0530 Subject: [PATCH] feat(stepfunctions-tasks): Support calling ApiGateway REST and HTTP APIs (#13033) feat(stepfunctions-tasks): Support calling APIGW REST and HTTP APIs Taking ownership of the original PR #11565 by @Sumeet-Badyal API as per documentation here: https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-control-access-using-iam-policies-to-invoke-api.html closes #11566 closes #11565 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-stepfunctions-tasks/README.md | 44 ++ .../lib/apigateway/base-types.ts | 79 ++++ .../lib/apigateway/base.ts | 69 +++ .../lib/apigateway/call-http-api.ts | 62 +++ .../lib/apigateway/call-rest-api.ts | 51 +++ .../lib/apigateway/index.ts | 3 + .../aws-stepfunctions-tasks/lib/index.ts | 1 + .../aws-stepfunctions-tasks/package.json | 6 + .../test/apigateway/call-http-api.test.ts | 145 +++++++ .../test/apigateway/call-rest-api.test.ts | 151 +++++++ .../integ.call-http-api.expected.json | 263 ++++++++++++ .../test/apigateway/integ.call-http-api.ts | 48 +++ .../integ.call-rest-api.expected.json | 394 ++++++++++++++++++ .../test/apigateway/integ.call-rest-api.ts | 43 ++ 14 files changed, 1359 insertions(+) create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/base-types.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/base.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/call-http-api.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/call-rest-api.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/index.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/call-http-api.test.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/call-rest-api.test.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-http-api.expected.json create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-http-api.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-rest-api.expected.json create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-rest-api.ts diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index 69d97936aabcb..8d024db58e552 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -28,6 +28,9 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aw - [ResultPath](#resultpath) - [Parameters](#task-parameters-from-the-state-json) - [Evaluate Expression](#evaluate-expression) +- [API Gateway](#api-gateway) + - [Call REST API Endpoint](#call-rest-api-endpoint) + - [Call HTTP API Endpoint](#call-http-api-endpoint) - [Athena](#athena) - [StartQueryExecution](#startQueryExecution) - [GetQueryExecution](#getQueryExecution) @@ -217,6 +220,47 @@ The `EvaluateExpression` supports a `runtime` prop to specify the Lambda runtime to use to evaluate the expression. Currently, only runtimes of the Node.js family are supported. +## API Gateway + +Step Functions supports [API Gateway](https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html) through the service integration pattern. + +HTTP APIs are designed for low-latency, cost-effective integrations with AWS services, including AWS Lambda, and HTTP endpoints. +HTTP APIs support OIDC and OAuth 2.0 authorization, and come with built-in support for CORS and automatic deployments. +Previous-generation REST APIs currently offer more features. More details can be found [here](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vs-rest.html). + +### Call REST API Endpoint + +The `CallApiGatewayRestApiEndpoint` calls the REST API endpoint. + +```ts +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as tasks from `@aws-cdk/aws-stepfunctions-tasks`; + +const restApi = new apigateway.RestApi(stack, 'MyRestApi'); + +const invokeTask = new tasks.CallApiGatewayRestApiEndpoint(stack, 'Call REST API', { + api: restApi, + stageName: 'prod', + method: HttpMethod.GET, +}); +``` + +### Call HTTP API Endpoint + +The `CallApiGatewayHttpApiEndpoint` calls the HTTP API endpoint. + +```ts +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as tasks from `@aws-cdk/aws-stepfunctions-tasks`; + +const httpApi = new apigatewayv2.HttpApi(stack, 'MyHttpApi'); + +const invokeTask = new tasks.CallApiGatewayHttpApiEndpoint(stack, 'Call HTTP API', { + api: httpApi, + method: HttpMethod.GET, +}); +``` + ## Athena Step Functions supports [Athena](https://docs.aws.amazon.com/step-functions/latest/dg/connect-athena.html) through the service integration pattern. diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/base-types.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/base-types.ts new file mode 100644 index 0000000000000..64c649063e57c --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/base-types.ts @@ -0,0 +1,79 @@ +import * as sfn from '@aws-cdk/aws-stepfunctions'; + +/** Http Methods that API Gateway supports */ +export enum HttpMethod { + /** Retreive data from a server at the specified resource */ + GET = 'GET', + + /** Send data to the API endpoint to create or udpate a resource */ + POST = 'POST', + + /** Send data to the API endpoint to update or create a resource */ + PUT = 'PUT', + + /** Delete the resource at the specified endpoint */ + DELETE = 'DELETE', + + /** Apply partial modifications to the resource */ + PATCH = 'PATCH', + + /** Retreive data from a server at the specified resource without the response body */ + HEAD = 'HEAD', + + /** Return data describing what other methods and operations the server supports */ + OPTIONS = 'OPTIONS' +} + +/** + * The authentication method used to call the endpoint + */ +export enum AuthType { + /** Call the API direclty with no authorization method */ + NO_AUTH = 'NO_AUTH', + + /** Use the IAM role associated with the current state machine for authorization */ + IAM_ROLE = 'IAM_ROLE', + + /** Use the resource policy of the API for authorization */ + RESOURCE_POLICY = 'RESOURCE_POLICY', +} + +/** + * Base CallApiGatewayEdnpoint Task Props + */ +export interface CallApiGatewayEndpointBaseProps extends sfn.TaskStateBaseProps { + /** + * Http method for the API + */ + readonly method: HttpMethod; + + /** + * HTTP request information that does not relate to contents of the request + * @default - No headers + */ + readonly headers?: sfn.TaskInput; + + /** + * Path parameters appended after API endpoint + * @default - No path + */ + readonly apiPath?: string; + + /** + * Query strings attatched to end of request + * @default - No query parameters + */ + readonly queryParameters?: sfn.TaskInput; + + /** + * HTTP Request body + * @default - No request body + */ + readonly requestBody?: sfn.TaskInput; + + /** + * Authentication methods + * @default AuthType.NO_AUTH + */ + readonly authType?: AuthType; +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/base.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/base.ts new file mode 100644 index 0000000000000..edce3aa0f627c --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/base.ts @@ -0,0 +1,69 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Construct } from 'constructs'; +import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; +import { AuthType, CallApiGatewayEndpointBaseProps } from './base-types'; + +/** + * Base CallApiGatewayEndpoint Task + * @internal + */ +export abstract class CallApiGatewayEndpointBase extends sfn.TaskStateBase { + private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ + sfn.IntegrationPattern.REQUEST_RESPONSE, + sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + ]; + + private readonly baseProps: CallApiGatewayEndpointBaseProps; + private readonly integrationPattern: sfn.IntegrationPattern; + + protected abstract readonly apiEndpoint: string; + protected abstract readonly arnForExecuteApi: string; + protected abstract readonly stageName?: string; + + constructor(scope: Construct, id: string, props: CallApiGatewayEndpointBaseProps) { + super(scope, id, props); + + this.baseProps = props; + this.integrationPattern = props.integrationPattern ?? sfn.IntegrationPattern.REQUEST_RESPONSE; + validatePatternSupported(this.integrationPattern, CallApiGatewayEndpointBase.SUPPORTED_INTEGRATION_PATTERNS); + + if (this.integrationPattern === sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN) { + if (!sfn.FieldUtils.containsTaskToken(this.baseProps.headers)) { + throw new Error('Task Token is required in `headers` for WAIT_FOR_TASK_TOKEN pattern. Use JsonPath.taskToken to set the token.'); + } + } + } + + /** + * @internal + */ + protected _renderTask() { + return { + Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + ApiEndpoint: this.apiEndpoint, + Method: this.baseProps.method, + Headers: this.baseProps.headers?.value, + Stage: this.stageName, + Path: this.baseProps.apiPath, + QueryParameters: this.baseProps.queryParameters?.value, + RequestBody: this.baseProps.requestBody?.value, + AuthType: this.baseProps.authType ? this.baseProps.authType : 'NO_AUTH', + }), + }; + } + + protected createPolicyStatements(): iam.PolicyStatement[] { + if (this.baseProps.authType === AuthType.NO_AUTH) { + return []; + } + + return [ + new iam.PolicyStatement({ + resources: [this.arnForExecuteApi], + actions: ['execute-api:Invoke'], + }), + ]; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/call-http-api.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/call-http-api.ts new file mode 100644 index 0000000000000..e06e46c2580b0 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/call-http-api.ts @@ -0,0 +1,62 @@ +import * as apigatewayv2 from '@aws-cdk/aws-apigatewayv2'; +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CallApiGatewayEndpointBase } from './base'; +import { CallApiGatewayEndpointBaseProps } from './base-types'; + +/** + * Properties for calling an HTTP API Endpoint + */ +export interface CallApiGatewayHttpApiEndpointProps extends CallApiGatewayEndpointBaseProps { + /** + * API to call + */ + readonly api: apigatewayv2.IHttpApi; + + /** + * Name of the stage where the API is deployed to in API Gateway + * @default '$default' + */ + readonly stageName?: string; +} + +/** + * Call HTTP API endpoint as a Task + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html + */ +export class CallApiGatewayHttpApiEndpoint extends CallApiGatewayEndpointBase { + protected readonly taskMetrics?: sfn.TaskMetricsConfig | undefined; + protected readonly taskPolicies?: iam.PolicyStatement[] | undefined; + + protected readonly apiEndpoint: string; + protected readonly arnForExecuteApi: string; + protected readonly stageName?: string; + + constructor(scope: Construct, id: string, private readonly props: CallApiGatewayHttpApiEndpointProps) { + super(scope, id, props); + + this.apiEndpoint = this.getApiEndpoint(); + this.arnForExecuteApi = this.getArnForExecuteApi(); + + this.taskPolicies = this.createPolicyStatements(); + } + + private getApiEndpoint(): string { + const apiStack = cdk.Stack.of(this.props.api); + return `${this.props.api.apiId}.execute-api.${apiStack.region}.${apiStack.urlSuffix}`; + } + + private getArnForExecuteApi(): string { + const { api, stageName, method, apiPath } = this.props; + + return cdk.Stack.of(api).formatArn({ + service: 'execute-api', + resource: api.apiId, + sep: '/', + resourceName: `${stageName}/${method}${apiPath}`, + }); + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/call-rest-api.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/call-rest-api.ts new file mode 100644 index 0000000000000..0352777e9c06a --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/call-rest-api.ts @@ -0,0 +1,51 @@ +import * as apigateway from '@aws-cdk/aws-apigateway'; +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CallApiGatewayEndpointBase } from './base'; +import { CallApiGatewayEndpointBaseProps } from './base-types'; + +/** + * Properties for calling an REST API Endpoint + */ +export interface CallApiGatewayRestApiEndpointProps extends CallApiGatewayEndpointBaseProps { + /** + * API to call + */ + readonly api: apigateway.IRestApi; + + /** + * Name of the stage where the API is deployed to in API Gateway + */ + readonly stageName: string; +} + +/** + * Call REST API endpoint as a Task + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html + */ +export class CallApiGatewayRestApiEndpoint extends CallApiGatewayEndpointBase { + protected readonly taskMetrics?: sfn.TaskMetricsConfig | undefined; + protected readonly taskPolicies?: iam.PolicyStatement[] | undefined; + + protected readonly apiEndpoint: string; + protected readonly arnForExecuteApi: string; + protected readonly stageName?: string; + + constructor(scope: Construct, id: string, private readonly props: CallApiGatewayRestApiEndpointProps) { + super(scope, id, props); + + this.apiEndpoint = this.getApiEndpoint(); + this.arnForExecuteApi = props.api.arnForExecuteApi(props.method, props.apiPath, props.stageName); + this.stageName = props.stageName; + + this.taskPolicies = this.createPolicyStatements(); + } + + private getApiEndpoint(): string { + const apiStack = cdk.Stack.of(this.props.api); + return `${this.props.api.restApiId}.execute-api.${apiStack.region}.${apiStack.urlSuffix}`; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/index.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/index.ts new file mode 100644 index 0000000000000..3d82ca2e7d548 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/index.ts @@ -0,0 +1,3 @@ +export * from './base-types'; +export * from './call-rest-api'; +export * from './call-http-api'; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts index 32e684f6d1adf..7b566bbbe4dad 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts @@ -45,3 +45,4 @@ export * from './athena/get-query-execution'; export * from './athena/get-query-results'; export * from './databrew/start-job-run'; export * from './eks/call'; +export * from './apigateway'; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json index a6137f570b1a3..b18cd8fc7704c 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json @@ -72,6 +72,9 @@ "pkglint": "0.0.0" }, "dependencies": { + "@aws-cdk/aws-apigateway": "0.0.0", + "@aws-cdk/aws-apigatewayv2": "0.0.0", + "@aws-cdk/aws-apigatewayv2-integrations": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-codebuild": "0.0.0", @@ -95,6 +98,9 @@ }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { + "@aws-cdk/aws-apigateway": "0.0.0", + "@aws-cdk/aws-apigatewayv2": "0.0.0", + "@aws-cdk/aws-apigatewayv2-integrations": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-codebuild": "0.0.0", diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/call-http-api.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/call-http-api.test.ts new file mode 100644 index 0000000000000..0e7a2cf616b9a --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/call-http-api.test.ts @@ -0,0 +1,145 @@ +import * as apigatewayv2 from '@aws-cdk/aws-apigatewayv2'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { HttpMethod, CallApiGatewayHttpApiEndpoint } from '../../lib'; + +describe('CallApiGatewayHttpApiEndpoint', () => { + test('default', () => { + // GIVEN + const stack = new cdk.Stack(); + const httpApi = new apigatewayv2.HttpApi(stack, 'HttpApi'); + + // WHEN + const task = new CallApiGatewayHttpApiEndpoint(stack, 'Call', { + api: httpApi, + method: HttpMethod.GET, + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + End: true, + Parameters: { + ApiEndpoint: { + 'Fn::Join': [ + '', + [ + { + Ref: 'HttpApiF5A9A8A7', + }, + '.execute-api.', + { + Ref: 'AWS::Region', + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + ], + ], + }, + AuthType: 'NO_AUTH', + Method: 'GET', + }, + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::apigateway:invoke', + ], + ], + }, + }); + }); + + test('wait for task token', () => { + // GIVEN + const stack = new cdk.Stack(); + const httpApi = new apigatewayv2.HttpApi(stack, 'HttpApi'); + + // WHEN + const task = new CallApiGatewayHttpApiEndpoint(stack, 'Call', { + api: httpApi, + method: HttpMethod.GET, + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + headers: sfn.TaskInput.fromObject({ TaskToken: sfn.JsonPath.taskToken }), + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + End: true, + Parameters: { + ApiEndpoint: { + 'Fn::Join': [ + '', + [ + { + Ref: 'HttpApiF5A9A8A7', + }, + '.execute-api.', + { + Ref: 'AWS::Region', + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + ], + ], + }, + AuthType: 'NO_AUTH', + Headers: { + 'TaskToken.$': '$$.Task.Token', + }, + Method: 'GET', + }, + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::apigateway:invoke.waitForTaskToken', + ], + ], + }, + }); + }); + + test('wait for task token - missing token', () => { + // GIVEN + const stack = new cdk.Stack(); + const httpApi = new apigatewayv2.HttpApi(stack, 'HttpApi'); + + // THEN + expect(() => { + new CallApiGatewayHttpApiEndpoint(stack, 'Call', { + api: httpApi, + method: HttpMethod.GET, + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + }); + }).toThrow(/Task Token is required in `headers` for WAIT_FOR_TASK_TOKEN pattern. Use JsonPath.taskToken to set the token./); + }); + + test('unsupported integration pattern - RUN_JOB', () => { + // GIVEN + const stack = new cdk.Stack(); + const httpApi = new apigatewayv2.HttpApi(stack, 'HttpApi'); + + // THEN + expect(() => { + new CallApiGatewayHttpApiEndpoint(stack, 'Call', { + api: httpApi, + method: HttpMethod.GET, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + }); + }).toThrow(/Unsupported service integration pattern./); + }); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/call-rest-api.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/call-rest-api.test.ts new file mode 100644 index 0000000000000..37a083fb2cc95 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/call-rest-api.test.ts @@ -0,0 +1,151 @@ +import * as apigateway from '@aws-cdk/aws-apigateway'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { HttpMethod, CallApiGatewayRestApiEndpoint } from '../../lib'; + +describe('CallApiGatewayRestApiEndpoint', () => { + test('default', () => { + // GIVEN + const stack = new cdk.Stack(); + const restApi = new apigateway.RestApi(stack, 'RestApi'); + + // WHEN + const task = new CallApiGatewayRestApiEndpoint(stack, 'Call', { + api: restApi, + method: HttpMethod.GET, + stageName: 'dev', + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + End: true, + Parameters: { + ApiEndpoint: { + 'Fn::Join': [ + '', + [ + { + Ref: 'RestApi0C43BF4B', + }, + '.execute-api.', + { + Ref: 'AWS::Region', + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + ], + ], + }, + AuthType: 'NO_AUTH', + Method: 'GET', + Stage: 'dev', + }, + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::apigateway:invoke', + ], + ], + }, + }); + }); + + test('wait for task token', () => { + // GIVEN + const stack = new cdk.Stack(); + const restApi = new apigateway.RestApi(stack, 'RestApi'); + + // WHEN + const task = new CallApiGatewayRestApiEndpoint(stack, 'Call', { + api: restApi, + method: HttpMethod.GET, + stageName: 'dev', + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + headers: sfn.TaskInput.fromObject({ TaskToken: sfn.JsonPath.taskToken }), + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + End: true, + Parameters: { + ApiEndpoint: { + 'Fn::Join': [ + '', + [ + { + Ref: 'RestApi0C43BF4B', + }, + '.execute-api.', + { + Ref: 'AWS::Region', + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + ], + ], + }, + AuthType: 'NO_AUTH', + Headers: { + 'TaskToken.$': '$$.Task.Token', + }, + Method: 'GET', + Stage: 'dev', + }, + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::apigateway:invoke.waitForTaskToken', + ], + ], + }, + }); + }); + + test('wait for task token - missing token', () => { + // GIVEN + const stack = new cdk.Stack(); + const restApi = new apigateway.RestApi(stack, 'RestApi'); + + // THEN + expect(() => { + new CallApiGatewayRestApiEndpoint(stack, 'Call', { + api: restApi, + method: HttpMethod.GET, + stageName: 'dev', + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + }); + }).toThrow(/Task Token is required in `headers` for WAIT_FOR_TASK_TOKEN pattern. Use JsonPath.taskToken to set the token./); + }); + + test('unsupported integration pattern - RUN_JOB', () => { + // GIVEN + const stack = new cdk.Stack(); + const restApi = new apigateway.RestApi(stack, 'RestApi'); + + // THEN + expect(() => { + new CallApiGatewayRestApiEndpoint(stack, 'Call', { + api: restApi, + method: HttpMethod.GET, + stageName: 'dev', + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + }); + }).toThrow(/Unsupported service integration pattern./); + }); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-http-api.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-http-api.expected.json new file mode 100644 index 0000000000000..6afe44cfecda5 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-http-api.expected.json @@ -0,0 +1,263 @@ +{ + "Resources": { + "MyHttpApi8AEAAC21": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "MyHttpApi", + "ProtocolType": "HTTP" + } + }, + "MyHttpApiDefaultStageDCB9BC49": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "MyHttpApi8AEAAC21" + }, + "StageName": "$default", + "AutoDeploy": true + } + }, + "MyHttpApiANYCallHttpApiIntegMyHttpApiANY7E6F12A3Permission59116CA6": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "HelloHandler2E4FBA4D", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "MyHttpApi8AEAAC21" + }, + "/*/*/" + ] + ] + } + } + }, + "MyHttpApiANYHttpIntegration71abbf75d6f8e5ea93ec2120c0d78b754BBCECF5": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "MyHttpApi8AEAAC21" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "HelloHandler2E4FBA4D", + "Arn" + ] + }, + "PayloadFormatVersion": "2.0" + } + }, + "MyHttpApiANYC3543576": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "MyHttpApi8AEAAC21" + }, + "RouteKey": "ANY /", + "AuthorizationScopes": [], + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "MyHttpApiANYHttpIntegration71abbf75d6f8e5ea93ec2120c0d78b754BBCECF5" + } + ] + ] + } + } + }, + "HelloHandlerServiceRole11EF7C63": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "HelloHandler2E4FBA4D": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) { return { statusCode: 200, body: \"hello, world!\" }; };" + }, + "Role": { + "Fn::GetAtt": [ + "HelloHandlerServiceRole11EF7C63", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs10.x" + }, + "DependsOn": [ + "HelloHandlerServiceRole11EF7C63" + ] + }, + "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" + } + } + }, + "StateMachineRoleDefaultPolicyDF1E6607": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "execute-api:Invoke", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "MyHttpApi8AEAAC21" + }, + "/undefined/GETundefined" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachineRoleDefaultPolicyDF1E6607", + "Roles": [ + { + "Ref": "StateMachineRoleB840431D" + } + ] + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Call APIGW\",\"States\":{\"Call APIGW\":{\"End\":true,\"Type\":\"Task\",\"OutputPath\":\"$.ResponseBody\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::apigateway:invoke\",\"Parameters\":{\"ApiEndpoint\":\"", + { + "Ref": "MyHttpApi8AEAAC21" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "\",\"Method\":\"GET\",\"AuthType\":\"IAM_ROLE\"}}},\"TimeoutSeconds\":30}" + ] + ] + } + }, + "DependsOn": [ + "StateMachineRoleDefaultPolicyDF1E6607", + "StateMachineRoleB840431D" + ] + } + }, + "Outputs": { + "stateMachineArn": { + "Value": { + "Ref": "StateMachine2E01A3A5" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-http-api.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-http-api.ts new file mode 100644 index 0000000000000..4eb1f3b896e92 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-http-api.ts @@ -0,0 +1,48 @@ +import * as apigatewayv2 from '@aws-cdk/aws-apigatewayv2'; +import * as integrations from '@aws-cdk/aws-apigatewayv2-integrations'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { AuthType, HttpMethod, CallApiGatewayHttpApiEndpoint } from '../../lib'; + +/* + * Stack verification steps: + * * aws stepfunctions start-execution --state-machine-arn : should return execution arn + * + * * aws stepfunctions describe-execution --execution-arn --query 'status': should return status as SUCCEEDED + * * aws stepfunctions describe-execution --execution-arn --query 'output': should return the string \"hello, world!\" + */ + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'CallHttpApiInteg'); +const httpApi = new apigatewayv2.HttpApi(stack, 'MyHttpApi'); + +const handler = new lambda.Function(stack, 'HelloHandler', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: new lambda.InlineCode('exports.handler = async function(event, context) { return { statusCode: 200, body: "hello, world!" }; };'), +}); +httpApi.addRoutes({ + path: '/', + integration: new integrations.LambdaProxyIntegration({ + handler, + }), +}); + +const callEndpointJob = new CallApiGatewayHttpApiEndpoint(stack, 'Call APIGW', { + api: httpApi, + method: HttpMethod.GET, + authType: AuthType.IAM_ROLE, + outputPath: sfn.JsonPath.stringAt('$.ResponseBody'), +}); + +const chain = sfn.Chain.start(callEndpointJob); + +const sm = new sfn.StateMachine(stack, 'StateMachine', { + definition: chain, + timeout: cdk.Duration.seconds(30), +}); + +new cdk.CfnOutput(stack, 'stateMachineArn', { + value: sm.stateMachineArn, +}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-rest-api.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-rest-api.expected.json new file mode 100644 index 0000000000000..5970499935354 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-rest-api.expected.json @@ -0,0 +1,394 @@ +{ + "Resources": { + "MyRestApi2D1F47A9": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "MyRestApi" + } + }, + "MyRestApiCloudWatchRoleD4042E8E": { + "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" + ] + ] + } + ] + } + }, + "MyRestApiAccount2FB6DB7A": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "MyRestApiCloudWatchRoleD4042E8E", + "Arn" + ] + } + }, + "DependsOn": [ + "MyRestApi2D1F47A9" + ] + }, + "MyRestApiDeploymentB555B582d61dc696e12272a0706c826196fa8d62": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "MyRestApiANY05143F93" + ] + }, + "MyRestApiDeploymentStageprodC33B8E5F": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "DeploymentId": { + "Ref": "MyRestApiDeploymentB555B582d61dc696e12272a0706c826196fa8d62" + }, + "StageName": "prod" + } + }, + "MyRestApiANYApiPermissionCallRestApiIntegMyRestApiB570839CANY0C27C1E3": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "Hello4A628BD4", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "MyRestApi2D1F47A9" + }, + "/", + { + "Ref": "MyRestApiDeploymentStageprodC33B8E5F" + }, + "/*/" + ] + ] + } + } + }, + "MyRestApiANYApiPermissionTestCallRestApiIntegMyRestApiB570839CANY379723EF": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "Hello4A628BD4", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "MyRestApi2D1F47A9" + }, + "/test-invoke-stage/*/" + ] + ] + } + } + }, + "MyRestApiANY05143F93": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Fn::GetAtt": [ + "MyRestApi2D1F47A9", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "Hello4A628BD4", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + } + }, + "HelloServiceRole1E55EA16": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "Hello4A628BD4": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) { return { statusCode: 200, body: \"hello, world!\" }; };" + }, + "Role": { + "Fn::GetAtt": [ + "HelloServiceRole1E55EA16", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs10.x" + }, + "DependsOn": [ + "HelloServiceRole1E55EA16" + ] + }, + "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" + } + } + }, + "StateMachineRoleDefaultPolicyDF1E6607": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "execute-api:Invoke", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "MyRestApi2D1F47A9" + }, + "/prod/GET/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachineRoleDefaultPolicyDF1E6607", + "Roles": [ + { + "Ref": "StateMachineRoleB840431D" + } + ] + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Call APIGW\",\"States\":{\"Call APIGW\":{\"End\":true,\"Type\":\"Task\",\"OutputPath\":\"$.ResponseBody\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::apigateway:invoke\",\"Parameters\":{\"ApiEndpoint\":\"", + { + "Ref": "MyRestApi2D1F47A9" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "\",\"Method\":\"GET\",\"Stage\":\"prod\",\"AuthType\":\"IAM_ROLE\"}}},\"TimeoutSeconds\":30}" + ] + ] + } + }, + "DependsOn": [ + "StateMachineRoleDefaultPolicyDF1E6607", + "StateMachineRoleB840431D" + ] + } + }, + "Outputs": { + "MyRestApiEndpoint4C55E4CB": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "MyRestApi2D1F47A9" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "MyRestApiDeploymentStageprodC33B8E5F" + }, + "/" + ] + ] + } + }, + "stateMachineArn": { + "Value": { + "Ref": "StateMachine2E01A3A5" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-rest-api.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-rest-api.ts new file mode 100644 index 0000000000000..7cfe3c85ab12b --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-rest-api.ts @@ -0,0 +1,43 @@ +import * as apigateway from '@aws-cdk/aws-apigateway'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { AuthType, HttpMethod, CallApiGatewayRestApiEndpoint } from '../../lib'; + +/* + * Stack verification steps: + * * aws stepfunctions start-execution --state-machine-arn : should return execution arn + * + * * aws stepfunctions describe-execution --execution-arn --query 'status': should return status as SUCCEEDED + * * aws stepfunctions describe-execution --execution-arn --query 'output': should return the string \"hello, world!\" + */ + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'CallRestApiInteg'); +const restApi = new apigateway.RestApi(stack, 'MyRestApi'); + +const hello = new apigateway.LambdaIntegration(new lambda.Function(stack, 'Hello', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: new lambda.InlineCode('exports.handler = async function(event, context) { return { statusCode: 200, body: "hello, world!" }; };'), +})); +restApi.root.addMethod('ANY', hello); + +const callEndpointJob = new CallApiGatewayRestApiEndpoint(stack, 'Call APIGW', { + api: restApi, + stageName: 'prod', + method: HttpMethod.GET, + authType: AuthType.IAM_ROLE, + outputPath: sfn.JsonPath.stringAt('$.ResponseBody'), +}); + +const chain = sfn.Chain.start(callEndpointJob); + +const sm = new sfn.StateMachine(stack, 'StateMachine', { + definition: chain, + timeout: cdk.Duration.seconds(30), +}); + +new cdk.CfnOutput(stack, 'stateMachineArn', { + value: sm.stateMachineArn, +});