From bc37725b4afdb278257cf05d139e7de09b6b5f34 Mon Sep 17 00:00:00 2001 From: Mathieu Gilbert Date: Thu, 11 Jan 2024 10:27:31 -0700 Subject: [PATCH] Add http:invoke step functions task --- .../lib/http/invoke.ts | 129 ++++++++++++++ .../aws-stepfunctions-tasks/lib/index.ts | 1 + .../test/http/invoke.test.ts | 157 ++++++++++++++++++ 3 files changed, 287 insertions(+) create mode 100644 packages/aws-cdk-lib/aws-stepfunctions-tasks/lib/http/invoke.ts create mode 100644 packages/aws-cdk-lib/aws-stepfunctions-tasks/test/http/invoke.test.ts diff --git a/packages/aws-cdk-lib/aws-stepfunctions-tasks/lib/http/invoke.ts b/packages/aws-cdk-lib/aws-stepfunctions-tasks/lib/http/invoke.ts new file mode 100644 index 0000000000000..0801a5aa2d1ed --- /dev/null +++ b/packages/aws-cdk-lib/aws-stepfunctions-tasks/lib/http/invoke.ts @@ -0,0 +1,129 @@ +import { Construct } from 'constructs'; +import * as iam from '../../../aws-iam'; +import * as sfn from '../../../aws-stepfunctions'; +import { integrationResourceArn } from '../private/task-utils'; + +export enum URLEncodingArrayFormat { + /** + * Encode arrays using brackets. For example, {"array": ["a","b","c"]} encodes to "array[]=a&array[]=b&array[]=c" + */ + BRACKETS = 'BRACKETS', + /** + * Encode arrays using commas. For example, {"array": ["a","b","c"]} encodes to "array=a,b,c,d" + */ + COMMAS = 'COMMAS', + /** + * Encode arrays using the index value. For example, {"array": ["a","b","c"]} encodes to "array[0]=a&array[1]=b&array[2]=c" + */ + INDICES = 'INDICES', + /** + * Repeat key for each item in the array. For example, {"array": ["a","b","c"]} encodes to "array[]=a&array[]=b&array[]=c" + */ + REPEAT = 'REPEAT', +} + +/** + * Properties for calling an external HTTP endpoint with HttpInvoke. + */ +export interface HttpInvokeProps extends sfn.TaskStateBaseProps { + /** + * The API apiEndpoint to call. + */ + readonly apiEndpoint: string; + + /** + * The HTTP method to use. + * + */ + readonly method: string; + + /** + * The EventBridge Connection ARN to use for authentication. + * + */ + readonly connectionArn: string; + + /** + * The body to send to the HTTP endpoint. + * + * @default - No body. + */ + readonly body?: string; + + /** + * The headers to send to the HTTP endpoint. + * + * @default - No headers. + */ + readonly headers?: { [key: string]: string }; + + /** + * The query string parameters to send to the HTTP endpoint. + * + * @default - No query string parameters. + */ + readonly queryStringParameters?: { [key: string]: string }; + + /** + * Whether to URL-encode the request body. + * If set to true, also sets 'content-type' header to 'application/x-www-form-urlencoded' + * + * @default - No encoding. + */ + readonly urlEncodeBody?: boolean; + + /** + * The format of the array encoding if urlEncodeBody is set to true. + * + * @default - ArrayEncodingFormat.INDICES + */ + readonly arrayEncodingFormat?: URLEncodingArrayFormat; +} + +export class HttpInvoke extends sfn.TaskStateBase { + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + constructor( + scope: Construct, + id: string, + private readonly props: HttpInvokeProps, + ) { + super(scope, id, props); + + this.taskPolicies = []; + } + + /** + * Provides the HTTP Invoke service integration task configuration. + */ + /** + * @internal + */ + protected _renderTask(): any { + return { + Resource: integrationResourceArn('http', 'invoke'), + Parameters: sfn.FieldUtils.renderObject({ + Method: this.props.method, + ApiEndpoint: this.props.apiEndpoint, + Authentication: { + ConnectionArn: this.props.connectionArn, + }, + RequestBody: this.props.body, + Headers: this.props.headers, + QueryParameters: this.props.queryStringParameters, + Transform: + this.props.urlEncodeBody != null + ? { + RequestBodyEncoding: 'URL_ENCODED', + RequestEncodingOptions: { + ArrayFormat: + this.props.arrayEncodingFormat ?? + URLEncodingArrayFormat.INDICES, + }, + } + : undefined, + }), + }; + } +} diff --git a/packages/aws-cdk-lib/aws-stepfunctions-tasks/lib/index.ts b/packages/aws-cdk-lib/aws-stepfunctions-tasks/lib/index.ts index 575464d22dcf0..907d10e0a8216 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions-tasks/lib/index.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions-tasks/lib/index.ts @@ -52,3 +52,4 @@ export * from './apigateway'; export * from './eventbridge/put-events'; export * from './aws-sdk/call-aws-service'; export * from './bedrock/invoke-model'; +export * from './http/invoke'; diff --git a/packages/aws-cdk-lib/aws-stepfunctions-tasks/test/http/invoke.test.ts b/packages/aws-cdk-lib/aws-stepfunctions-tasks/test/http/invoke.test.ts new file mode 100644 index 0000000000000..c30670a6d12c8 --- /dev/null +++ b/packages/aws-cdk-lib/aws-stepfunctions-tasks/test/http/invoke.test.ts @@ -0,0 +1,157 @@ +import { Stack } from '../../../core'; +import * as lib from '../../lib'; + +let stack: Stack; +const connectionArn = +'arn:aws:events:us-test-1:123456789012:connection/connectionName'; + +const expectTaskWithParameters = (task: lib.HttpInvoke, parameters: any) => { + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::http:invoke', + ], + ], + }, + End: true, + Parameters: parameters, + }); +}; + +describe('AWS::StepFunctions::Tasks::HttpInvoke', () => { + beforeEach(() => { + stack = new Stack(); + }); + + test('invoke with default props', () => { + const task = new lib.HttpInvoke(stack, 'Task', { + apiEndpoint: 'https://api.example.com', + connectionArn, + method: 'POST', + }); + + expectTaskWithParameters(task, { + ApiEndpoint: 'https://api.example.com', + Authentication: { + ConnectionArn: connectionArn, + }, + Method: 'POST', + }); + }); + + test('invoke with request body', () => { + const task = new lib.HttpInvoke(stack, 'Task', { + apiEndpoint: 'https://api.example.com', + body: JSON.stringify({ foo: 'bar' }), + connectionArn, + method: 'POST', + }); + + expectTaskWithParameters(task, { + ApiEndpoint: 'https://api.example.com', + Authentication: { + ConnectionArn: connectionArn, + }, + Method: 'POST', + RequestBody: JSON.stringify({ foo: 'bar' }), + }); + }); + + test('invoke with headers', () => { + const task = new lib.HttpInvoke(stack, 'Task', { + apiEndpoint: 'https://api.example.com', + connectionArn, + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + }); + + expectTaskWithParameters(task, { + ApiEndpoint: 'https://api.example.com', + Authentication: { + ConnectionArn: connectionArn, + }, + Method: 'POST', + Headers: { + 'Content-Type': 'application/json', + }, + }); + }); + + test('invoke with query string parameters', () => { + const task = new lib.HttpInvoke(stack, 'Task', { + apiEndpoint: 'https://api.example.com', + connectionArn, + method: 'POST', + queryStringParameters: { + foo: 'bar', + }, + }); + + expectTaskWithParameters(task, { + ApiEndpoint: 'https://api.example.com', + Authentication: { + ConnectionArn: connectionArn, + }, + Method: 'POST', + QueryParameters: { + foo: 'bar', + }, + }); + }); + + test('invoke with request body encoding and default arrayEncodingFormat', () => { + const task = new lib.HttpInvoke(stack, 'Task', { + apiEndpoint: 'https://api.example.com', + method: 'POST', + connectionArn, + urlEncodeBody: true, + }); + + expectTaskWithParameters(task, { + ApiEndpoint: 'https://api.example.com', + Authentication: { + ConnectionArn: connectionArn, + }, + Method: 'POST', + Transform: { + RequestBodyEncoding: 'URL_ENCODED', + RequestEncodingOptions: { + ArrayFormat: lib.URLEncodingArrayFormat.INDICES, + }, + }, + }); + }); + + test('invoke with request body encoding and arrayEncodingFormat', () => { + const task = new lib.HttpInvoke(stack, 'Task', { + apiEndpoint: 'https://api.example.com', + arrayEncodingFormat: lib.URLEncodingArrayFormat.BRACKETS, + connectionArn, + method: 'POST', + urlEncodeBody: true, + }); + + expectTaskWithParameters(task, { + ApiEndpoint: 'https://api.example.com', + Authentication: { + ConnectionArn: connectionArn, + }, + Method: 'POST', + Transform: { + RequestBodyEncoding: 'URL_ENCODED', + RequestEncodingOptions: { + ArrayFormat: lib.URLEncodingArrayFormat.BRACKETS, + }, + }, + }); + }); +});