From 51e47d8b1da7928807c199a40eb70f840e68c124 Mon Sep 17 00:00:00 2001 From: Sumeet Badyal Date: Wed, 18 Nov 2020 16:02:26 -0800 Subject: [PATCH 1/8] feat: add APIGW Invoke to SFN-tasks --- .../aws-stepfunctions-tasks/README.md | 21 + .../lib/apigateway/invoke.ts | 277 ++++++++++++ .../aws-stepfunctions-tasks/lib/index.ts | 1 + .../apigateway/integ.invoke.expected.json | 65 +++ .../test/apigateway/integ.invoke.ts | 29 ++ .../test/apigateway/invoke.test.ts | 396 ++++++++++++++++++ 6 files changed, 789 insertions(+) create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/invoke.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.expected.json create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/invoke.test.ts diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index 269135eed0652..053d2fe13a7d5 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -26,6 +26,8 @@ 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) + - [Invoke](#invoke) - [Athena](#athena) - [StartQueryExecution](#startQueryExecution) - [GetQueryExecution](#getQueryExecution) @@ -211,6 +213,25 @@ runtime to use to evaluate the expression. Currently, the only runtime supported is `lambda.Runtime.NODEJS_10_X`. +## 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. + +### Invoke + +The [Invoke](https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html) API calls the API endpoint. + +```ts +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as tasks from `@aws-cdk/aws-stepfunctions-tasks`; + +const invokeJob = new tasks.ApiGatewayInvoke(stack, 'Invoke APIGW', { + apiEndpoint: 'APIID.execute-api.REGION.amazonaws.com', + stage: 'prod', + method: ApiGatewayMethodType.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/invoke.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/invoke.ts new file mode 100644 index 0000000000000..984f25d33faf8 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/invoke.ts @@ -0,0 +1,277 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; + +/** + * Properties for invoking an API Endpoint with ApiGatewayInvoke + */ +export interface ApiGatewayInvokeProps extends sfn.TaskStateBaseProps { + + /** + * hostname of an API Gateway URL + * @example {ApiId}.execute-api.{region}.amazonaws.com + */ + readonly apiEndpoint: string; + + /** + * Http method for the API + */ + readonly method: HttpMethod; + + /** + * HTTP headers string to list of strings + * @default - No headers + */ + readonly headers?: sfn.TaskInput; + + /** + * Name of the stage where the API is deployed to in API Gateway + * @default - Required for REST and $default for HTTP + */ + readonly stage?: string; + + /** + * Path parameters appended after API endpoint + * @default - No path + */ + readonly path?: string; + + /** + * Query strings string to list of strings + * @default - No query parameters + */ + readonly queryParameters?: sfn.TaskInput; + + /** + * HTTP Request body + * @default - No requestBody + */ + readonly requestBody?: sfn.TaskInput; + /** + * Authentication methods + * + * NO_AUTH: call the API direclty with no authorization method + * + * IAM_ROLE: Use the IAM role associated with the current state machine for authorization + * + * RESOURCE_POLICY: Use the resource policy of the API for authorization + * + * @default - NO_AUTH + */ + readonly authType?: sfn.TaskInput; + +} +/** + * Invoke an API endpoint as a Task + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html + */ +export class ApiGatewayInvoke extends sfn.TaskStateBase { + + private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ + sfn.IntegrationPattern.REQUEST_RESPONSE, + sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + ]; + + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + private readonly integrationPattern: sfn.IntegrationPattern; + + constructor(scope: Construct, id: string, private readonly props: ApiGatewayInvokeProps) { + super(scope, id, props); + this.integrationPattern = props.integrationPattern ?? sfn.IntegrationPattern.REQUEST_RESPONSE; + + validatePatternSupported(this.integrationPattern, ApiGatewayInvoke.SUPPORTED_INTEGRATION_PATTERNS); + const authType = this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value; + if (authType === 'IAM_ROLE') { + const resource = props.apiEndpoint.split('.', 1)[0] + '/' + (props.stage ? props.stage + '/' : '$default/') + props.method + '/' + (props.path ?? ''); + + this.taskPolicies = [ + new iam.PolicyStatement({ + resources: [ + Stack.of(this).formatArn({ + service: 'execute-api', + resource: resource, + }), + ], + actions: ['execute-api:Invoke'], + }), + ]; + } else if (authType === 'RESOURCE_POLICY') { + if (!sfn.FieldUtils.containsTaskToken(props.headers)) { + throw new Error('Task Token is required in `headers` Use JsonPath.taskToken to set the token.'); + } + const resource = props.apiEndpoint.split('.', 1)[0] + '/' + (props.stage ? props.stage + '/' : '') + props.method + '/' + (props.path ? props.path + '/*' : '*'); + + this.taskPolicies = [ + new iam.PolicyStatement({ + resources: [ + Stack.of(this).formatArn({ + service: 'execute-api', + resource: resource, + }), + ], + actions: ['execute-api:Invoke'], + conditions: { + StringEquals: { + 'aws:SourceArn': '*', + }, + }, + }), + ]; + } + } + + /** + * Provides the API Gateway Invoke service integration task configuration + */ + /** + * @internal + */ + protected _renderTask(): any { + if (this.props.headers && this.props.queryParameters && this.props.requestBody) { + return { + Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + ApiEndpoint: this.props.apiEndpoint, + Method: this.props.method, + Headers: this.props.headers ? this.props.headers.value : sfn.TaskInput.fromDataAt('$.Headers').value, + Stage: this.props.stage, + Path: this.props.path, + QueryParameters: this.props.queryParameters ? this.props.queryParameters.value : sfn.TaskInput.fromDataAt('$.QueryParameters').value, + RequestBody: this.props.requestBody ? this.props.requestBody.value : sfn.TaskInput.fromDataAt('$.RequestBody').value, + AuthType: (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) === '$' ? (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) : 'NO_AUTH', + }), + }; + } else if (this.props.headers && this.props.queryParameters) { + return { + Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + ApiEndpoint: this.props.apiEndpoint, + Method: this.props.method, + Headers: this.props.headers ? this.props.headers.value : sfn.TaskInput.fromDataAt('$.Headers').value, + Stage: this.props.stage, + Path: this.props.path, + QueryParameters: this.props.queryParameters ? this.props.queryParameters.value : sfn.TaskInput.fromDataAt('$.QueryParameters').value, + AuthType: (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) === '$' ? (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) : 'NO_AUTH', + }), + }; + } else if (this.props.queryParameters && this.props.requestBody) { + return { + Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + ApiEndpoint: this.props.apiEndpoint, + Method: this.props.method, + Stage: this.props.stage, + Path: this.props.path, + QueryParameters: this.props.queryParameters ? this.props.queryParameters.value : sfn.TaskInput.fromDataAt('$.QueryParameters').value, + RequestBody: this.props.requestBody ? this.props.requestBody.value : sfn.TaskInput.fromDataAt('$.RequestBody').value, + AuthType: (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) === '$' ? (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) : 'NO_AUTH', + }), + }; + } else if (this.props.headers && this.props.requestBody) { + return { + Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + ApiEndpoint: this.props.apiEndpoint, + Method: this.props.method, + Headers: this.props.headers ? this.props.headers.value : sfn.TaskInput.fromDataAt('$.Headers').value, + Stage: this.props.stage, + Path: this.props.path, + RequestBody: this.props.requestBody ? this.props.requestBody.value : sfn.TaskInput.fromDataAt('$.RequestBody').value, + AuthType: (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) === '$' ? (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) : 'NO_AUTH', + }), + }; + } else if (this.props.headers) { + return { + Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + ApiEndpoint: this.props.apiEndpoint, + Method: this.props.method, + Headers: this.props.headers ? this.props.headers.value : sfn.TaskInput.fromDataAt('$.Headers').value, + Stage: this.props.stage, + Path: this.props.path, + AuthType: (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) === '$' ? (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) : 'NO_AUTH', + }), + }; + } else if (this.props.queryParameters) { + return { + Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + ApiEndpoint: this.props.apiEndpoint, + Method: this.props.method, + Stage: this.props.stage, + Path: this.props.path, + QueryParameters: this.props.queryParameters ? this.props.queryParameters.value : sfn.TaskInput.fromDataAt('$.QueryParameters').value, + AuthType: (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) === '$' ? (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) : 'NO_AUTH', + }), + }; + } else if (this.props.requestBody) { + return { + Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + ApiEndpoint: this.props.apiEndpoint, + Method: this.props.method, + Stage: this.props.stage, + Path: this.props.path, + RequestBody: this.props.requestBody ? this.props.requestBody.value : sfn.TaskInput.fromDataAt('$.RequestBody').value, + AuthType: (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) === '$' ? (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) : 'NO_AUTH', + }), + }; + } else { + return { + Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + ApiEndpoint: this.props.apiEndpoint, + Method: this.props.method, + Stage: this.props.stage, + Path: this.props.path, + AuthType: (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) === '$' ? (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) : 'NO_AUTH', + }), + }; + } + } +} + +/** + * 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' +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts index 8e3567f2a8f88..196d4dfecaa2d 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts @@ -43,3 +43,4 @@ export * from './athena/start-query-execution'; export * from './athena/stop-query-execution'; export * from './athena/get-query-execution'; export * from './athena/get-query-results'; +export * from './apigateway/invoke'; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.expected.json new file mode 100644 index 0000000000000..a8c8bb23cb311 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.expected.json @@ -0,0 +1,65 @@ +{ + "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": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Invoke APIGW\",\"States\":{\"Invoke APIGW\":{\"End\":true,\"Type\":\"Task\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::apigateway:invoke\",\"Parameters\":{\"ApiEndpoint\":\"apiid.execute-api.us-east-1.amazonaws.com\",\"Method\":\"GET\",\"Stage\":\"prod\",\"AuthType\":\"NO_AUTH\"}}},\"TimeoutSeconds\":30}" + ] + ] + } + }, + "DependsOn": [ + "StateMachineRoleB840431D" + ] + } + }, + "Outputs": { + "stateMachineArn": { + "Value": { + "Ref": "StateMachine2E01A3A5" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.ts new file mode 100644 index 0000000000000..6af39a4f34745 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.ts @@ -0,0 +1,29 @@ +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { ApiGatewayInvoke, HttpMethod } from '../../lib'; + +/* + * Stack verification steps: + * * aws stepfunctions start-execution --state-machine-arn : should return execution arn + * * aws stepfunctions describe-execution --execution-arn : should return status as SUCCEEDED and a query-execution-id + * * aws apigateway test-invoke-method --rest-api-id --resource-id --http-method : should return the same response as the state machine + */ +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-stepfunctions-tasks-apigateway-invoke-integ'); + +const invokeJob = new ApiGatewayInvoke(stack, 'Invoke APIGW', { + apiEndpoint: 'apiid.execute-api.us-east-1.amazonaws.com', + stage: 'prod', + method: HttpMethod.GET, +}); + +const chain = sfn.Chain.start(invokeJob); + +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/invoke.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/invoke.test.ts new file mode 100644 index 0000000000000..b9995221f2e4c --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/invoke.test.ts @@ -0,0 +1,396 @@ +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { ApiGatewayInvoke, HttpMethod } from '../../lib/apigateway/invoke'; + +describe('Invoke API', () => { + + test('default settings', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const task = new ApiGatewayInvoke(stack, 'Invoke', { + apiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', + method: HttpMethod.GET, + stage: 'default', + path: 'path', + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::apigateway:invoke', + ], + ], + }, + End: true, + Parameters: { + ApiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', + Method: HttpMethod.GET, + Stage: 'default', + AuthType: 'NO_AUTH', + Path: 'path', + }, + }); + }); + + test('Wait for Task Token', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskToken = { + 'TaskToken.$': 'States.Array($$.Task.Token)', + }; + + // WHEN + const task = new ApiGatewayInvoke(stack, 'Invoke', { + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + apiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', + method: HttpMethod.GET, + headers: sfn.TaskInput.fromObject(taskToken), + stage: 'default', + path: 'path', + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::apigateway:invoke.waitForTaskToken', + ], + ], + }, + End: true, + Parameters: { + ApiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', + Headers: { + 'TaskToken.$': 'States.Array($$.Task.Token)', + }, + Method: HttpMethod.GET, + AuthType: 'NO_AUTH', + Path: 'path', + Stage: 'default', + }, + }); + }); + + test('default settings with headers, query parameters and request body', () => { + // GIVEN + const stack = new cdk.Stack(); + const requestBody = { + billId: 'my-new-bill', + }; + const queryParameters = { + billId: '123456', + }; + const header = { + key: 'value', + }; + + // WHEN + const task = new ApiGatewayInvoke(stack, 'Invoke', { + apiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', + method: HttpMethod.GET, + stage: 'default', + path: 'path', + queryParameters: sfn.TaskInput.fromObject(queryParameters), + headers: sfn.TaskInput.fromObject(header), + requestBody: sfn.TaskInput.fromObject(requestBody), + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::apigateway:invoke', + ], + ], + }, + End: true, + Parameters: { + ApiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', + Method: HttpMethod.GET, + Stage: 'default', + AuthType: 'NO_AUTH', + Path: 'path', + RequestBody: { + billId: 'my-new-bill', + }, + QueryParameters: { + billId: '123456', + }, + Headers: { + key: 'value', + }, + }, + }); + }); + + test('default settings with headers and query parameters', () => { + // GIVEN + const stack = new cdk.Stack(); + const requestBody = { + billId: 'my-new-bill', + }; + const header = { + key: 'value', + }; + + // WHEN + const task = new ApiGatewayInvoke(stack, 'Invoke', { + apiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', + method: HttpMethod.GET, + stage: 'default', + path: 'path', + headers: sfn.TaskInput.fromObject(header), + requestBody: sfn.TaskInput.fromObject(requestBody), + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::apigateway:invoke', + ], + ], + }, + End: true, + Parameters: { + ApiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', + Method: HttpMethod.GET, + Stage: 'default', + AuthType: 'NO_AUTH', + Path: 'path', + RequestBody: { + billId: 'my-new-bill', + }, + Headers: { + key: 'value', + }, + }, + }); + }); + + test('default settings with headers and request body', () => { + // GIVEN + const stack = new cdk.Stack(); + const queryParameters = { + billId: '123456', + }; + const header = { + key: 'value', + }; + + // WHEN + const task = new ApiGatewayInvoke(stack, 'Invoke', { + apiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', + method: HttpMethod.GET, + stage: 'default', + path: 'path', + queryParameters: sfn.TaskInput.fromObject(queryParameters), + headers: sfn.TaskInput.fromObject(header), + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::apigateway:invoke', + ], + ], + }, + End: true, + Parameters: { + ApiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', + Method: HttpMethod.GET, + Stage: 'default', + AuthType: 'NO_AUTH', + Path: 'path', + QueryParameters: { + billId: '123456', + }, + Headers: { + key: 'value', + }, + }, + }); + }); + + test('default settings with query parameters and request body', () => { + // GIVEN + const stack = new cdk.Stack(); + const requestBody = { + billId: 'my-new-bill', + }; + const queryParameters = { + billId: '123456', + }; + + // WHEN + const task = new ApiGatewayInvoke(stack, 'Invoke', { + apiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', + method: HttpMethod.GET, + stage: 'default', + path: 'path', + queryParameters: sfn.TaskInput.fromObject(queryParameters), + requestBody: sfn.TaskInput.fromObject(requestBody), + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::apigateway:invoke', + ], + ], + }, + End: true, + Parameters: { + ApiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', + Method: HttpMethod.GET, + Stage: 'default', + AuthType: 'NO_AUTH', + Path: 'path', + RequestBody: { + billId: 'my-new-bill', + }, + QueryParameters: { + billId: '123456', + }, + }, + }); + }); + + + test('default settings with request body', () => { + // GIVEN + const stack = new cdk.Stack(); + const requestBody = { + billId: 'my-new-bill', + }; + + // WHEN + const task = new ApiGatewayInvoke(stack, 'Invoke', { + apiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', + method: HttpMethod.GET, + stage: 'default', + path: 'path', + requestBody: sfn.TaskInput.fromObject(requestBody), + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::apigateway:invoke', + ], + ], + }, + End: true, + Parameters: { + ApiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', + Method: HttpMethod.GET, + Stage: 'default', + AuthType: 'NO_AUTH', + Path: 'path', + RequestBody: { + billId: 'my-new-bill', + }, + }, + }); + }); + + test('default settings with query parameters', () => { + // GIVEN + const stack = new cdk.Stack(); + const queryParameters = { + billId: '123456', + }; + + // WHEN + const task = new ApiGatewayInvoke(stack, 'Invoke', { + apiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', + method: HttpMethod.GET, + stage: 'default', + path: 'path', + queryParameters: sfn.TaskInput.fromObject(queryParameters), + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::apigateway:invoke', + ], + ], + }, + End: true, + Parameters: { + ApiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', + Method: HttpMethod.GET, + Stage: 'default', + AuthType: 'NO_AUTH', + Path: 'path', + QueryParameters: { + billId: '123456', + }, + }, + }); + }); +}); \ No newline at end of file From fbb40d6e3b853ed625615f66a435ec4169bb778d Mon Sep 17 00:00:00 2001 From: Sumeet Badyal Date: Thu, 19 Nov 2020 12:13:33 -0800 Subject: [PATCH 2/8] Use IRestApi & resolve other comments --- .../lib/apigateway/invoke.ts | 283 ++++++------- .../apigateway/integ.invoke.expected.json | 331 +++++++++++++++- .../test/apigateway/integ.invoke.ts | 25 +- .../test/apigateway/invoke.test.ts | 372 +++--------------- 4 files changed, 527 insertions(+), 484 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/invoke.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/invoke.ts index 984f25d33faf8..a3b22b682ff66 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/invoke.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/invoke.ts @@ -1,6 +1,7 @@ +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 { Stack } from '@aws-cdk/core'; +import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; @@ -9,28 +10,36 @@ import { integrationResourceArn, validatePatternSupported } from '../private/tas */ export interface ApiGatewayInvokeProps extends sfn.TaskStateBaseProps { + /** API to call */ + readonly api: apigateway.IRestApi; + /** * hostname of an API Gateway URL * @example {ApiId}.execute-api.{region}.amazonaws.com */ readonly apiEndpoint: string; - /** - * Http method for the API - */ + /** Http method for the API */ readonly method: HttpMethod; /** - * HTTP headers string to list of strings + * HTTP request information that does not relate to contents of the request * @default - No headers + * @example + * Headers: { + * type: 1, + * value:{ + * 'TaskToken.$': 'States.Array($$.Task.Token)', + * } + * }, */ - readonly headers?: sfn.TaskInput; + readonly headers?: { [key: string]: any }; /** * Name of the stage where the API is deployed to in API Gateway * @default - Required for REST and $default for HTTP */ - readonly stage?: string; + readonly stageName?: string; /** * Path parameters appended after API endpoint @@ -39,30 +48,33 @@ export interface ApiGatewayInvokeProps extends sfn.TaskStateBaseProps { readonly path?: string; /** - * Query strings string to list of strings + * Query strings attatched to end of request * @default - No query parameters + * @example + * "QueryParameters": { + * "billId": ["123", "456"] + * }, */ - readonly queryParameters?: sfn.TaskInput; + readonly queryParameters?: { [key: string]: any }; /** * HTTP Request body * @default - No requestBody + * @example + * "RequestBody": { + * "billId": ["my-new-bill"] + * }, */ readonly requestBody?: sfn.TaskInput; + /** * Authentication methods - * - * NO_AUTH: call the API direclty with no authorization method - * - * IAM_ROLE: Use the IAM role associated with the current state machine for authorization - * - * RESOURCE_POLICY: Use the resource policy of the API for authorization - * * @default - NO_AUTH */ - readonly authType?: sfn.TaskInput; + readonly authType?: AuthType; } + /** * Invoke an API endpoint as a Task * @@ -74,9 +86,9 @@ export class ApiGatewayInvoke extends sfn.TaskStateBase { sfn.IntegrationPattern.REQUEST_RESPONSE, sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, ]; - protected readonly taskMetrics?: sfn.TaskMetricsConfig; protected readonly taskPolicies?: iam.PolicyStatement[]; + protected readonly apiEndpoint: string; private readonly integrationPattern: sfn.IntegrationPattern; @@ -85,36 +97,75 @@ export class ApiGatewayInvoke extends sfn.TaskStateBase { this.integrationPattern = props.integrationPattern ?? sfn.IntegrationPattern.REQUEST_RESPONSE; validatePatternSupported(this.integrationPattern, ApiGatewayInvoke.SUPPORTED_INTEGRATION_PATTERNS); - const authType = this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value; - if (authType === 'IAM_ROLE') { - const resource = props.apiEndpoint.split('.', 1)[0] + '/' + (props.stage ? props.stage + '/' : '$default/') + props.method + '/' + (props.path ?? ''); - this.taskPolicies = [ + this.taskPolicies = this.createPolicyStatements(); + this.apiEndpoint = this.createApiEndpoint(); + } + + /** + * Provides the API Gateway Invoke service integration task configuration + */ + /** + * @internal + */ + protected _renderTask(): any { + return { + Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + ApiEndpoint: this.apiEndpoint, + Method: this.props.method, + Headers: this.props.headers, + Stage: this.props.stageName, + Path: this.props.path, + QueryParameters: this.props.queryParameters, + RequestBody: this.props.requestBody, + AuthType: this.props.authType ? this.props.authType : 'NO_AUTH', + }), + }; + } + + /** + * Gets the "execute-api" ARN + * @returns The "execute-api" ARN. + * @default "*" returns the execute API ARN for all methods/resources in + * this API. + * @param method The method (default `*`) + * @param path The resource path. Must start with '/' (default `*`) + * @param stage The stage (default `*`) + */ + get arnForExecuteApi() { + return this.props.api.arnForExecuteApi(this.props.method, this.props.path, this.props.stageName); + } + + /** + * Generates the api endpoint + * @returns The api id + * @example {ApiId}.execute-api.{region}.amazonaws.com + */ + private createApiEndpoint(): string { + const apiStack = cdk.Stack.of(this.props.api); + return `${this.props.api.restApiId}.execute-api.${apiStack.region}.${apiStack.urlSuffix}`; + } + + /** + * This generates the PolicyStatements required by the Task to call invoke. + */ + private createPolicyStatements(): iam.PolicyStatement[] { + if (this.props.authType === AuthType.IAM_ROLE) { + return [ new iam.PolicyStatement({ - resources: [ - Stack.of(this).formatArn({ - service: 'execute-api', - resource: resource, - }), - ], - actions: ['execute-api:Invoke'], + resources: [this.arnForExecuteApi], + actions: ['ExecuteAPI:Invoke'], }), ]; - } else if (authType === 'RESOURCE_POLICY') { - if (!sfn.FieldUtils.containsTaskToken(props.headers)) { + } else if (this.props.authType === AuthType.RESOURCE_POLICY) { + if (!sfn.FieldUtils.containsTaskToken(this.props.headers)) { throw new Error('Task Token is required in `headers` Use JsonPath.taskToken to set the token.'); } - const resource = props.apiEndpoint.split('.', 1)[0] + '/' + (props.stage ? props.stage + '/' : '') + props.method + '/' + (props.path ? props.path + '/*' : '*'); - - this.taskPolicies = [ + return [ new iam.PolicyStatement({ - resources: [ - Stack.of(this).formatArn({ - service: 'execute-api', - resource: resource, - }), - ], - actions: ['execute-api:Invoke'], + resources: [this.arnForExecuteApi], + actions: ['ExecuteAPI:Invoke'], conditions: { StringEquals: { 'aws:SourceArn': '*', @@ -123,116 +174,7 @@ export class ApiGatewayInvoke extends sfn.TaskStateBase { }), ]; } - } - - /** - * Provides the API Gateway Invoke service integration task configuration - */ - /** - * @internal - */ - protected _renderTask(): any { - if (this.props.headers && this.props.queryParameters && this.props.requestBody) { - return { - Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), - Parameters: sfn.FieldUtils.renderObject({ - ApiEndpoint: this.props.apiEndpoint, - Method: this.props.method, - Headers: this.props.headers ? this.props.headers.value : sfn.TaskInput.fromDataAt('$.Headers').value, - Stage: this.props.stage, - Path: this.props.path, - QueryParameters: this.props.queryParameters ? this.props.queryParameters.value : sfn.TaskInput.fromDataAt('$.QueryParameters').value, - RequestBody: this.props.requestBody ? this.props.requestBody.value : sfn.TaskInput.fromDataAt('$.RequestBody').value, - AuthType: (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) === '$' ? (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) : 'NO_AUTH', - }), - }; - } else if (this.props.headers && this.props.queryParameters) { - return { - Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), - Parameters: sfn.FieldUtils.renderObject({ - ApiEndpoint: this.props.apiEndpoint, - Method: this.props.method, - Headers: this.props.headers ? this.props.headers.value : sfn.TaskInput.fromDataAt('$.Headers').value, - Stage: this.props.stage, - Path: this.props.path, - QueryParameters: this.props.queryParameters ? this.props.queryParameters.value : sfn.TaskInput.fromDataAt('$.QueryParameters').value, - AuthType: (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) === '$' ? (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) : 'NO_AUTH', - }), - }; - } else if (this.props.queryParameters && this.props.requestBody) { - return { - Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), - Parameters: sfn.FieldUtils.renderObject({ - ApiEndpoint: this.props.apiEndpoint, - Method: this.props.method, - Stage: this.props.stage, - Path: this.props.path, - QueryParameters: this.props.queryParameters ? this.props.queryParameters.value : sfn.TaskInput.fromDataAt('$.QueryParameters').value, - RequestBody: this.props.requestBody ? this.props.requestBody.value : sfn.TaskInput.fromDataAt('$.RequestBody').value, - AuthType: (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) === '$' ? (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) : 'NO_AUTH', - }), - }; - } else if (this.props.headers && this.props.requestBody) { - return { - Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), - Parameters: sfn.FieldUtils.renderObject({ - ApiEndpoint: this.props.apiEndpoint, - Method: this.props.method, - Headers: this.props.headers ? this.props.headers.value : sfn.TaskInput.fromDataAt('$.Headers').value, - Stage: this.props.stage, - Path: this.props.path, - RequestBody: this.props.requestBody ? this.props.requestBody.value : sfn.TaskInput.fromDataAt('$.RequestBody').value, - AuthType: (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) === '$' ? (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) : 'NO_AUTH', - }), - }; - } else if (this.props.headers) { - return { - Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), - Parameters: sfn.FieldUtils.renderObject({ - ApiEndpoint: this.props.apiEndpoint, - Method: this.props.method, - Headers: this.props.headers ? this.props.headers.value : sfn.TaskInput.fromDataAt('$.Headers').value, - Stage: this.props.stage, - Path: this.props.path, - AuthType: (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) === '$' ? (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) : 'NO_AUTH', - }), - }; - } else if (this.props.queryParameters) { - return { - Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), - Parameters: sfn.FieldUtils.renderObject({ - ApiEndpoint: this.props.apiEndpoint, - Method: this.props.method, - Stage: this.props.stage, - Path: this.props.path, - QueryParameters: this.props.queryParameters ? this.props.queryParameters.value : sfn.TaskInput.fromDataAt('$.QueryParameters').value, - AuthType: (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) === '$' ? (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) : 'NO_AUTH', - }), - }; - } else if (this.props.requestBody) { - return { - Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), - Parameters: sfn.FieldUtils.renderObject({ - ApiEndpoint: this.props.apiEndpoint, - Method: this.props.method, - Stage: this.props.stage, - Path: this.props.path, - RequestBody: this.props.requestBody ? this.props.requestBody.value : sfn.TaskInput.fromDataAt('$.RequestBody').value, - AuthType: (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) === '$' ? (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) : 'NO_AUTH', - }), - }; - } else { - return { - Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), - Parameters: sfn.FieldUtils.renderObject({ - ApiEndpoint: this.props.apiEndpoint, - Method: this.props.method, - Stage: this.props.stage, - Path: this.props.path, - AuthType: (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) === '$' ? (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) : 'NO_AUTH', - }), - }; - } + return []; } } @@ -245,33 +187,36 @@ export enum HttpMethod { */ GET = 'GET', - /** - * Send data to the API endpoint to create or udpate a resource - */ + /** 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 - */ + /** Send data to the API endpoint to update or create a resource */ PUT = 'PUT', - /** - * Delete the resource at the specified endpoint - */ + /** Delete the resource at the specified endpoint */ DELETE = 'DELETE', - /** - * Apply partial modifications to the resource - */ + /** Apply partial modifications to the resource */ PATCH = 'PATCH', - /** - * Retreive data from a server at the specified resource without the response body - */ + /** 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 - */ + /** Return data describing what other methods and operations the server supports */ OPTIONS = 'OPTIONS' } + +/** + * The authentication method used to call the endpoint + * @default NO_AUTH + */ +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', +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.expected.json index a8c8bb23cb311..ca507e207c129 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.expected.json @@ -1,5 +1,251 @@ { "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" + } + }, + "MyRestApiANYApiPermissionawsstepfunctionstasksapigatewayinvokeintegMyRestApi2586FD6CANYF85B0B1B": { + "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" + }, + "/*/" + ] + ] + } + } + }, + "MyRestApiANYApiPermissionTestawsstepfunctionstasksapigatewayinvokeintegMyRestApi2586FD6CANY2BA29F0F": { + "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 = function helloCode(_event, _context, callback) {\n return callback(undefined, {\n statusCode: 200,\n body: 'hello, world!',\n });\n}" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "HelloServiceRole1E55EA16", + "Arn" + ] + }, + "Runtime": "nodejs10.x" + }, + "DependsOn": [ + "HelloServiceRole1E55EA16" + ] + }, "StateMachineRoleB840431D": { "Type": "AWS::IAM::Role", "Properties": { @@ -28,6 +274,50 @@ } } }, + "StateMachineRoleDefaultPolicyDF1E6607": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "ExecuteAPI: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": { @@ -45,17 +335,56 @@ { "Ref": "AWS::Partition" }, - ":states:::apigateway:invoke\",\"Parameters\":{\"ApiEndpoint\":\"apiid.execute-api.us-east-1.amazonaws.com\",\"Method\":\"GET\",\"Stage\":\"prod\",\"AuthType\":\"NO_AUTH\"}}},\"TimeoutSeconds\":30}" + ":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" diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.ts index 6af39a4f34745..e2b574367adb2 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.ts @@ -1,6 +1,8 @@ +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 { ApiGatewayInvoke, HttpMethod } from '../../lib'; +import { ApiGatewayInvoke, AuthType, HttpMethod } from '../../lib'; /* * Stack verification steps: @@ -10,11 +12,28 @@ import { ApiGatewayInvoke, HttpMethod } from '../../lib'; */ const app = new cdk.App(); const stack = new cdk.Stack(app, 'aws-stepfunctions-tasks-apigateway-invoke-integ'); +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: lambda.Code.inline(`exports.handler = ${helloCode}`), +})); + +function helloCode(_event: any, _context: any, callback: any) { + return callback(undefined, { + statusCode: 200, + body: 'hello, world!', + }); +} +restApi.root.addMethod('ANY', hello); const invokeJob = new ApiGatewayInvoke(stack, 'Invoke APIGW', { - apiEndpoint: 'apiid.execute-api.us-east-1.amazonaws.com', - stage: 'prod', + api: restApi, + apiEndpoint: restApi.restApiId, + stageName: 'prod', method: HttpMethod.GET, + authType: AuthType.IAM_ROLE, }); const chain = sfn.Chain.start(invokeJob); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/invoke.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/invoke.test.ts index b9995221f2e4c..78123fdb9afcf 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/invoke.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/invoke.test.ts @@ -1,18 +1,21 @@ +import * as apigateway from '@aws-cdk/aws-apigateway'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; -import { ApiGatewayInvoke, HttpMethod } from '../../lib/apigateway/invoke'; +import { ApiGatewayInvoke, AuthType, HttpMethod } from '../../lib/apigateway/invoke'; describe('Invoke API', () => { test('default settings', () => { // GIVEN const stack = new cdk.Stack(); + const restApi = new apigateway.RestApi(stack, 'apiid.execute-api.region.amazonaws.com'); // WHEN const task = new ApiGatewayInvoke(stack, 'Invoke', { - apiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', + api: restApi, + apiEndpoint: restApi.restApiId, method: HttpMethod.GET, - stage: 'default', + stageName: '$default', path: 'path', }); @@ -33,9 +36,26 @@ describe('Invoke API', () => { }, End: true, Parameters: { - ApiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', + ApiEndpoint: { + 'Fn::Join': [ + '', + [ + { + Ref: 'apiidexecuteapiregionamazonawscomF3F21B4A', + }, + '.execute-api.', + { + Ref: 'AWS::Region', + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + ], + ], + }, Method: HttpMethod.GET, - Stage: 'default', + Stage: '$default', AuthType: 'NO_AUTH', Path: 'path', }, @@ -45,6 +65,7 @@ describe('Invoke API', () => { test('Wait for Task Token', () => { // GIVEN const stack = new cdk.Stack(); + const restApi = new apigateway.RestApi(stack, 'apiid.execute-api.region.amazonaws.com'); const taskToken = { 'TaskToken.$': 'States.Array($$.Task.Token)', }; @@ -52,10 +73,11 @@ describe('Invoke API', () => { // WHEN const task = new ApiGatewayInvoke(stack, 'Invoke', { integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, - apiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', + api: restApi, + apiEndpoint: restApi.restApiId, method: HttpMethod.GET, headers: sfn.TaskInput.fromObject(taskToken), - stage: 'default', + stageName: '$default', path: 'path', }); @@ -76,321 +98,49 @@ describe('Invoke API', () => { }, End: true, Parameters: { - ApiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', - Headers: { - 'TaskToken.$': 'States.Array($$.Task.Token)', - }, - Method: HttpMethod.GET, - AuthType: 'NO_AUTH', - Path: 'path', - Stage: 'default', - }, - }); - }); - - test('default settings with headers, query parameters and request body', () => { - // GIVEN - const stack = new cdk.Stack(); - const requestBody = { - billId: 'my-new-bill', - }; - const queryParameters = { - billId: '123456', - }; - const header = { - key: 'value', - }; - - // WHEN - const task = new ApiGatewayInvoke(stack, 'Invoke', { - apiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', - method: HttpMethod.GET, - stage: 'default', - path: 'path', - queryParameters: sfn.TaskInput.fromObject(queryParameters), - headers: sfn.TaskInput.fromObject(header), - requestBody: sfn.TaskInput.fromObject(requestBody), - }); - - // THEN - expect(stack.resolve(task.toStateJson())).toEqual({ - Type: 'Task', - Resource: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':states:::apigateway:invoke', + ApiEndpoint: { + 'Fn::Join': [ + '', + [ + { + Ref: 'apiidexecuteapiregionamazonawscomF3F21B4A', + }, + '.execute-api.', + { + Ref: 'AWS::Region', + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + ], ], - ], - }, - End: true, - Parameters: { - ApiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', - Method: HttpMethod.GET, - Stage: 'default', - AuthType: 'NO_AUTH', - Path: 'path', - RequestBody: { - billId: 'my-new-bill', - }, - QueryParameters: { - billId: '123456', }, Headers: { - key: 'value', + type: 1, + value:{ + 'TaskToken.$': 'States.Array($$.Task.Token)', + } }, - }, - }); - }); - - test('default settings with headers and query parameters', () => { - // GIVEN - const stack = new cdk.Stack(); - const requestBody = { - billId: 'my-new-bill', - }; - const header = { - key: 'value', - }; - - // WHEN - const task = new ApiGatewayInvoke(stack, 'Invoke', { - apiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', - method: HttpMethod.GET, - stage: 'default', - path: 'path', - headers: sfn.TaskInput.fromObject(header), - requestBody: sfn.TaskInput.fromObject(requestBody), - }); - - // THEN - expect(stack.resolve(task.toStateJson())).toEqual({ - Type: 'Task', - Resource: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':states:::apigateway:invoke', - ], - ], - }, - End: true, - Parameters: { - ApiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', Method: HttpMethod.GET, - Stage: 'default', AuthType: 'NO_AUTH', Path: 'path', - RequestBody: { - billId: 'my-new-bill', - }, - Headers: { - key: 'value', - }, + Stage: '$default', }, }); }); - test('default settings with headers and request body', () => { - // GIVEN + test('Invoke - Wait For Task Token - Missing Task Token', () => { const stack = new cdk.Stack(); - const queryParameters = { - billId: '123456', - }; - const header = { - key: 'value', - }; - - // WHEN - const task = new ApiGatewayInvoke(stack, 'Invoke', { - apiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', - method: HttpMethod.GET, - stage: 'default', - path: 'path', - queryParameters: sfn.TaskInput.fromObject(queryParameters), - headers: sfn.TaskInput.fromObject(header), - }); - - // THEN - expect(stack.resolve(task.toStateJson())).toEqual({ - Type: 'Task', - Resource: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':states:::apigateway:invoke', - ], - ], - }, - End: true, - Parameters: { - ApiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', - Method: HttpMethod.GET, - Stage: 'default', - AuthType: 'NO_AUTH', - Path: 'path', - QueryParameters: { - billId: '123456', - }, - Headers: { - key: 'value', - }, - }, - }); - }); - - test('default settings with query parameters and request body', () => { - // GIVEN - const stack = new cdk.Stack(); - const requestBody = { - billId: 'my-new-bill', - }; - const queryParameters = { - billId: '123456', - }; - - // WHEN - const task = new ApiGatewayInvoke(stack, 'Invoke', { - apiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', - method: HttpMethod.GET, - stage: 'default', - path: 'path', - queryParameters: sfn.TaskInput.fromObject(queryParameters), - requestBody: sfn.TaskInput.fromObject(requestBody), - }); - - // THEN - expect(stack.resolve(task.toStateJson())).toEqual({ - Type: 'Task', - Resource: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':states:::apigateway:invoke', - ], - ], - }, - End: true, - Parameters: { - ApiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', - Method: HttpMethod.GET, - Stage: 'default', - AuthType: 'NO_AUTH', - Path: 'path', - RequestBody: { - billId: 'my-new-bill', - }, - QueryParameters: { - billId: '123456', - }, - }, - }); - }); - - - test('default settings with request body', () => { - // GIVEN - const stack = new cdk.Stack(); - const requestBody = { - billId: 'my-new-bill', - }; - - // WHEN - const task = new ApiGatewayInvoke(stack, 'Invoke', { - apiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', - method: HttpMethod.GET, - stage: 'default', - path: 'path', - requestBody: sfn.TaskInput.fromObject(requestBody), - }); - - // THEN - expect(stack.resolve(task.toStateJson())).toEqual({ - Type: 'Task', - Resource: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':states:::apigateway:invoke', - ], - ], - }, - End: true, - Parameters: { - ApiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', - Method: HttpMethod.GET, - Stage: 'default', - AuthType: 'NO_AUTH', - Path: 'path', - RequestBody: { - billId: 'my-new-bill', - }, - }, - }); - }); - - test('default settings with query parameters', () => { - // GIVEN - const stack = new cdk.Stack(); - const queryParameters = { - billId: '123456', - }; - - // WHEN - const task = new ApiGatewayInvoke(stack, 'Invoke', { - apiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', - method: HttpMethod.GET, - stage: 'default', - path: 'path', - queryParameters: sfn.TaskInput.fromObject(queryParameters), - }); - - // THEN - expect(stack.resolve(task.toStateJson())).toEqual({ - Type: 'Task', - Resource: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':states:::apigateway:invoke', - ], - ], - }, - End: true, - Parameters: { - ApiEndpoint: 'apiid.execute-api.{region}.amazonaws.com', - Method: HttpMethod.GET, - Stage: 'default', - AuthType: 'NO_AUTH', - Path: 'path', - QueryParameters: { - billId: '123456', - }, - }, - }); + const restApi = new apigateway.RestApi(stack, 'apiid.execute-api.region.amazonaws.com'); + expect(() => { + new ApiGatewayInvoke(stack, 'Invoke', { + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + api: restApi, + apiEndpoint: restApi.restApiId, + method: HttpMethod.GET, + authType: AuthType.RESOURCE_POLICY, + }); + }).toThrow('Task Token is required in `headers` Use JsonPath.taskToken to set the token.'); }); }); \ No newline at end of file From e14080a408949a73d6edb854222b27dac28a30fb Mon Sep 17 00:00:00 2001 From: Sumeet Badyal Date: Thu, 19 Nov 2020 12:18:43 -0800 Subject: [PATCH 3/8] update README.md --- packages/@aws-cdk/aws-stepfunctions-tasks/README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index 053d2fe13a7d5..6dd5005cd5a48 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -225,9 +225,12 @@ The [Invoke](https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-ga 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 invokeJob = new tasks.ApiGatewayInvoke(stack, 'Invoke APIGW', { - apiEndpoint: 'APIID.execute-api.REGION.amazonaws.com', - stage: 'prod', + api: restApi, + apiEndpoint: restApi.restApiId, + stageName: 'prod', method: ApiGatewayMethodType.GET, }); ``` From 32d0e26a5fdbaa4c79bec44ec654e8454288d143 Mon Sep 17 00:00:00 2001 From: Sumeet Badyal Date: Thu, 19 Nov 2020 12:23:23 -0800 Subject: [PATCH 4/8] Make comments for HttpMethod and HttpMethod.GET single line --- .../aws-stepfunctions-tasks/lib/apigateway/invoke.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/invoke.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/invoke.ts index a3b22b682ff66..18261e9f35a4a 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/invoke.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/invoke.ts @@ -178,13 +178,9 @@ export class ApiGatewayInvoke extends sfn.TaskStateBase { } } -/** - * Http Methods that API Gateway supports - */ +/** Http Methods that API Gateway supports */ export enum HttpMethod { - /** - * Retreive data from a server at the specified resource - */ + /** Retreive data from a server at the specified resource */ GET = 'GET', /** Send data to the API endpoint to create or udpate a resource */ From 494334bb69ab0a34ce26a0adb354717f11a6dd2a Mon Sep 17 00:00:00 2001 From: Sumeet Badyal Date: Thu, 19 Nov 2020 12:32:32 -0800 Subject: [PATCH 5/8] linting errors --- .../aws-stepfunctions-tasks/test/apigateway/invoke.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/invoke.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/invoke.test.ts index 78123fdb9afcf..468436e0c253f 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/invoke.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/invoke.test.ts @@ -118,9 +118,9 @@ describe('Invoke API', () => { }, Headers: { type: 1, - value:{ + value: { 'TaskToken.$': 'States.Array($$.Task.Token)', - } + }, }, Method: HttpMethod.GET, AuthType: 'NO_AUTH', From ac29551b9662e6b4a1db56b8c2a293c9207f82bd Mon Sep 17 00:00:00 2001 From: Sumeet Badyal Date: Thu, 19 Nov 2020 12:58:07 -0800 Subject: [PATCH 6/8] Fix waitForTaskToken unit test --- .../aws-stepfunctions-tasks/test/apigateway/invoke.test.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/invoke.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/invoke.test.ts index 468436e0c253f..96b717bbbc3fe 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/invoke.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/invoke.test.ts @@ -76,7 +76,7 @@ describe('Invoke API', () => { api: restApi, apiEndpoint: restApi.restApiId, method: HttpMethod.GET, - headers: sfn.TaskInput.fromObject(taskToken), + headers: taskToken, stageName: '$default', path: 'path', }); @@ -117,10 +117,7 @@ describe('Invoke API', () => { ], }, Headers: { - type: 1, - value: { - 'TaskToken.$': 'States.Array($$.Task.Token)', - }, + 'TaskToken.$': 'States.Array($$.Task.Token)', }, Method: HttpMethod.GET, AuthType: 'NO_AUTH', From c47bb7e8d41d0440e93039878613d5ce9f8bafb1 Mon Sep 17 00:00:00 2001 From: Sumeet Badyal Date: Thu, 19 Nov 2020 13:08:33 -0800 Subject: [PATCH 7/8] Add aws-apigateway to package.json --- packages/@aws-cdk/aws-stepfunctions-tasks/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json index 9d7dca5eee6fd..99dd28480d26d 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json @@ -74,6 +74,7 @@ }, "dependencies": { "@aws-cdk/assets": "0.0.0", + "@aws-cdk/aws-apigateway": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-codebuild": "0.0.0", @@ -96,6 +97,7 @@ "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { "@aws-cdk/assets": "0.0.0", + "@aws-cdk/aws-apigateway": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-codebuild": "0.0.0", From 9cd40c72b1438c9acd2bf57e3cbd3bfe95dcc0dc Mon Sep 17 00:00:00 2001 From: Sumeet Badyal Date: Thu, 3 Dec 2020 11:27:52 -0800 Subject: [PATCH 8/8] Remove apiEndpoint and other comments --- .../aws-stepfunctions-tasks/README.md | 1 - .../lib/apigateway/invoke.ts | 21 ++++++------------- .../test/apigateway/integ.invoke.ts | 16 +++++++------- .../test/apigateway/invoke.test.ts | 3 --- 4 files changed, 14 insertions(+), 27 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index 6dd5005cd5a48..ecf995be46d3d 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -229,7 +229,6 @@ const restApi = new apigateway.RestApi(stack, 'MyRestApi'); const invokeJob = new tasks.ApiGatewayInvoke(stack, 'Invoke APIGW', { api: restApi, - apiEndpoint: restApi.restApiId, stageName: 'prod', method: ApiGatewayMethodType.GET, }); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/invoke.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/invoke.ts index 18261e9f35a4a..389f457b4fc20 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/invoke.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/invoke.ts @@ -13,12 +13,6 @@ export interface ApiGatewayInvokeProps extends sfn.TaskStateBaseProps { /** API to call */ readonly api: apigateway.IRestApi; - /** - * hostname of an API Gateway URL - * @example {ApiId}.execute-api.{region}.amazonaws.com - */ - readonly apiEndpoint: string; - /** Http method for the API */ readonly method: HttpMethod; @@ -37,7 +31,7 @@ export interface ApiGatewayInvokeProps extends sfn.TaskStateBaseProps { /** * Name of the stage where the API is deployed to in API Gateway - * @default - Required for REST and $default for HTTP + * @default - Required for REST and $default for HTTP which acts as a catch-all for requests that don’t match any other routes */ readonly stageName?: string; @@ -69,7 +63,7 @@ export interface ApiGatewayInvokeProps extends sfn.TaskStateBaseProps { /** * Authentication methods - * @default - NO_AUTH + * @default AuthType.NO_AUTH */ readonly authType?: AuthType; @@ -104,8 +98,6 @@ export class ApiGatewayInvoke extends sfn.TaskStateBase { /** * Provides the API Gateway Invoke service integration task configuration - */ - /** * @internal */ protected _renderTask(): any { @@ -125,13 +117,13 @@ export class ApiGatewayInvoke extends sfn.TaskStateBase { } /** - * Gets the "execute-api" ARN - * @returns The "execute-api" ARN. - * @default "*" returns the execute API ARN for all methods/resources in - * this API. + * Gets the resource to attatched to the ExecuteAPI:Invoke + * @returns resource. + * @default "*" returns the execute API ARN for all methods/resources in this API. * @param method The method (default `*`) * @param path The resource path. Must start with '/' (default `*`) * @param stage The stage (default `*`) + * @example {ApiId}/{stage}/{method}/{path} */ get arnForExecuteApi() { return this.props.api.arnForExecuteApi(this.props.method, this.props.path, this.props.stageName); @@ -204,7 +196,6 @@ export enum HttpMethod { /** * The authentication method used to call the endpoint - * @default NO_AUTH */ export enum AuthType { /** Call the API direclty with no authorization method */ diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.ts index e2b574367adb2..b8c5d9ea0ee50 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.ts @@ -8,29 +8,29 @@ import { ApiGatewayInvoke, AuthType, HttpMethod } from '../../lib'; * Stack verification steps: * * aws stepfunctions start-execution --state-machine-arn : should return execution arn * * aws stepfunctions describe-execution --execution-arn : should return status as SUCCEEDED and a query-execution-id - * * aws apigateway test-invoke-method --rest-api-id --resource-id --http-method : should return the same response as the state machine + * * aws apigateway test-invoke-method --rest-api-id --resource-id --http-method : should return the same response body and status as in the task succeeded/failed execution */ const app = new cdk.App(); const stack = new cdk.Stack(app, 'aws-stepfunctions-tasks-apigateway-invoke-integ'); 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: lambda.Code.inline(`exports.handler = ${helloCode}`), -})); - function helloCode(_event: any, _context: any, callback: any) { return callback(undefined, { statusCode: 200, body: 'hello, world!', }); } + +const hello = new apigateway.LambdaIntegration(new lambda.Function(stack, 'Hello', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.inline(`exports.handler = ${helloCode}`), +})); + restApi.root.addMethod('ANY', hello); const invokeJob = new ApiGatewayInvoke(stack, 'Invoke APIGW', { api: restApi, - apiEndpoint: restApi.restApiId, stageName: 'prod', method: HttpMethod.GET, authType: AuthType.IAM_ROLE, diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/invoke.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/invoke.test.ts index 96b717bbbc3fe..9e28cddb790d8 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/invoke.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/invoke.test.ts @@ -13,7 +13,6 @@ describe('Invoke API', () => { // WHEN const task = new ApiGatewayInvoke(stack, 'Invoke', { api: restApi, - apiEndpoint: restApi.restApiId, method: HttpMethod.GET, stageName: '$default', path: 'path', @@ -74,7 +73,6 @@ describe('Invoke API', () => { const task = new ApiGatewayInvoke(stack, 'Invoke', { integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, api: restApi, - apiEndpoint: restApi.restApiId, method: HttpMethod.GET, headers: taskToken, stageName: '$default', @@ -134,7 +132,6 @@ describe('Invoke API', () => { new ApiGatewayInvoke(stack, 'Invoke', { integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, api: restApi, - apiEndpoint: restApi.restApiId, method: HttpMethod.GET, authType: AuthType.RESOURCE_POLICY, });