diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md b/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md index cce77fd6398e6..8a60e1589f5aa 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md @@ -149,6 +149,57 @@ const httpEndpoint = new HttpApi(stack, 'HttpProxyPrivateApi', { }); ``` +### AWS Service Integrations + +AWS Service integrations allow for API Gateway to integrate directly with the following AWS Services: + +- EventBridge + - PutEvents +- SQS + - SendMessage + - ReceiveMessage + - DeleteMessage + - PurgeQueue +- Kinesis + - PutRecord +- Step Functions + - StartExecution + - StartSyncExecution + - StopExecution + +The following code configures a `message` route, which creates an SQS message containing the request body: + +```ts +const queue = new Queue(stack, 'Queue'); +const httpApi = new HttpApi(stack, 'IntegrationApi'); + +const role = new Role(stack, 'SQSRole', { + assumedBy: new ServicePrincipal('apigateway.amazonaws.com'), +}); +role.addToPrincipalPolicy(new PolicyStatement({ + actions: ['sqs:*'], + resources: [queue.queueArn], +})); + +httpApi.addRoutes({ + path: '/message', + methods: [HttpMethod.POST], + integration: new SqsSendMessageIntegration({ + role, + body: StringMappingExpression.fromMapping(Mapping.fromRequestBody()), + queue: QueueMappingExpression.fromQueue(queue), + }), +}); +``` + +Integrations should always specify a role, with appropriate permissions to allow the actions. + +All other integration properties, except for the `region` can be either set up by the CDK, or +specified in the request, context variables or stage variables. The various `MappingExpression` +classes assist with creating these properties; each can be constructed from the type it represents, +or from a `Mapping` from the request, context or scope, as described in the +[API Gateway documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-aws-services.html#http-api-develop-integrations-aws-services-parameter-mapping). + ## WebSocket APIs WebSocket integrations connect a route to backend resources. The following integrations are supported in the CDK. diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/aws-proxy.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/aws-proxy.ts new file mode 100644 index 0000000000000..20d83a2346950 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/aws-proxy.ts @@ -0,0 +1,580 @@ +import { HttpIntegrationSubtype, HttpIntegrationType, HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig, IHttpRouteIntegration, IntegrationCredentials, PayloadFormatVersion } from '@aws-cdk/aws-apigatewayv2'; +import { IRole, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; +import { ArrayMappingExpression, DateMappingExpression, DurationMappingExpression, EventBusMappingExpression, Mapping, QueueMappingExpression, SqsAttributeListMappingExpression, StateMachineMappingExpression, StreamMappingExpression, StringMappingExpression } from './mapping-expression'; + +interface AwsServiceIntegrationProps { + /** + * The credentials to use for the integration + */ + readonly role: IRole; + /** + * The region of the integration, cannot be an expression. + * @default - undefined + */ + readonly region?: string; +} + +abstract class AwsServiceIntegration implements + IHttpRouteIntegration { + protected payloadFormatVersion = PayloadFormatVersion.VERSION_1_0; + protected integrationType = HttpIntegrationType.LAMBDA_PROXY; + constructor(protected readonly props: T) { } + abstract bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig; +} + +/** + * Properties for EventBridge PutEvents Integrations. + * + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-aws-services-reference.html#EventBridge-PutEvents + */ +export interface EventBridgePutEventsIntegrationProps extends AwsServiceIntegrationProps { + // mapping: EventBridgePutEventMappingBuilder; + /** + * How to map the event detail. + */ + readonly detail: StringMappingExpression; + /** + * How to map the event detail-type. + */ + readonly detailType: StringMappingExpression; + /** + * How to map the event source. + */ + readonly source: StringMappingExpression; + /** + * How to map the event timestamp. Expects an RFC3339 timestamp. + * + * @default - the timestamp of the PutEvents call is used + */ + readonly time?: DateMappingExpression; + /** + * The event bus to receive the event. + * + * @default - the default event bus + */ + readonly eventBus?: EventBusMappingExpression; + /** + * AWS resources, identified by Amazon Resource Name (ARN), which the event primarily concerns. + * + * Must be a string representation of a JSON array, either containing static values or an expression. + * + * @default - none + */ + readonly resources?: ArrayMappingExpression; + /** + * An AWS X-Ray trade header, which is an http header (X-Amzn-Trace-Id) that contains the + * trace-id associated with the event. + * + * @default - none + */ + readonly traceHeader?: StringMappingExpression; +} + +/** + * An integration with EventBridge-PutEvents + */ +export class EventBridgePutEventsIntegration + extends AwsServiceIntegration { + constructor(props: EventBridgePutEventsIntegrationProps) { + super(props); + } + bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { + const role = this.props.role ?? new Role(options.scope, 'Role', { + assumedBy: new ServicePrincipal('apigateway.amazonaws.com'), + }); + return { + type: this.integrationType, + subtype: HttpIntegrationSubtype.EVENTBRIDGE_PUTEVENTS, + payloadFormatVersion: this.payloadFormatVersion, + credentials: IntegrationCredentials.fromRole(role), + requestParameters: { + Detail: this.props.detail.mapping, + DetailType: this.props.detailType.mapping, + Source: this.props.source.mapping, + Time: this.props.time?.mapping, + EventBusName: this.props.eventBus?.mapping, + Region: this.props.region, + Resources: this.props.resources?.mapping, + TraceHeader: this.props.traceHeader?.mapping, + }, + }; + } +} + +interface SqsIntegrationProps extends AwsServiceIntegrationProps { + /** The SQS Queue to send messages to */ + readonly queue: QueueMappingExpression; +} + +/** + * Properties for the SQS-SendMessage integration. + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-aws-services-reference.html#SQS-SendMessage + */ +export interface SqsSendMessageIntegrationProps extends SqsIntegrationProps { + /** The message body */ + readonly body: StringMappingExpression; + /** + * The message send delay, up to 15 minutes. + * Messages with a positive DelaySeconds value become available for processing after the delay + * period is finished. If you don't specify a value, the default value for the queue applies. + * + * @default - undefined + */ + readonly delay?: DurationMappingExpression, + /** + * Attributes of the message. + * + * @see https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-message-metadata.html#sqs-message-attributes + * @default - undefined + */ + readonly attributes?: SqsAttributeListMappingExpression; + /** + * The token used for deduplication of sent messages. + * This parameter applies only to FIFO (first-in-first-out) queues. + * @default - undefined + */ + readonly deduplicationId?: StringMappingExpression; + /** + * The tag that specifies that a message belongs to a specific message group. + * This parameter applies only to FIFO (first-in-first-out) queues. + * @default - undefined + */ + readonly groupId?: StringMappingExpression; + /** + * The message system attribute to send. + * Currently, the only supported message system attribute is AWSTraceHeader. + * Its type must be String and its value must be a correctly formatted AWS X-Ray trace header + * string. + * @default - undefined + */ + readonly systemAttributes?: StringMappingExpression; +} + +/** + * An SQS-SendMessage Integration. + */ +export class SqsSendMessageIntegration + extends AwsServiceIntegration { + constructor(props: SqsSendMessageIntegrationProps) { + super(props); + } + bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { + const role = this.props.role ?? new Role(options.scope, 'Role', { + assumedBy: new ServicePrincipal('apigateway.amazonaws.com'), + }); + return { + type: this.integrationType, + subtype: HttpIntegrationSubtype.SQS_SENDMESSAGE, + payloadFormatVersion: this.payloadFormatVersion, + credentials: IntegrationCredentials.fromRole(role), + requestParameters: { + QueueUrl: this.props.queue.mapping, + MessageBody: this.props.body.mapping, + DelaySeconds: this.props.delay?.mapping, + MessageAttributes: this.props.attributes?.mapping, + MessageDeduplicationId: this.props.deduplicationId?.mapping, + MessageGroupId: this.props.groupId?.mapping, + MessageSystemAttributes: this.props.systemAttributes?.mapping, + Region: this.props.region, + }, + }; + } +} + +/** + * The available SQS Attributes. + */ +export enum SqsMessageAttribute { + /** All */ + ALL = 'All', + /** Policy */ + POLICY = 'Policy', + /** VisibilityTimeout */ + VISIBILITY_TIMEOUT = 'VisibilityTimeout', + /** MaximumMessageSize */ + MAXIMUM_MESSAGE_SIZE = 'MaximumMessageSize', + /** MessageRetentionPeriod */ + MESSAGE_RETENTION_PERIOD = 'MessageRetentionPeriod', + /** ApproximateNumberOfMessages */ + APPROXIMATE_NUMBER_OF_MESSAGES = 'ApproximateNumberOfMessages', + /** ApproximateNumberOfMessagesNotVisible */ + APPROXIMATE_NUMBER_OF_MESSAGES_NOT_VISIBLE = 'ApproximateNumberOfMessagesNotVisible', + /** CreatedTimestamp */ + CREATED_TIMESTAMP = 'CreatedTimestamp', + /** LastModifiedTimestamp */ + LAST_MODIFIED_TIMESTAMP = 'LastModifiedTimestamp', + /** QueueArn */ + QUEUE_ARN = 'QueueArn', + /** ApproximateNumberOfMessagesDelayed */ + APPROXIMATE_NUMBER_OF_MESSAGES_DELAYED = 'ApproximateNumberOfMessagesDelayed', + /** DelaySeconds */ + DELAY_SECONDS = 'DelaySeconds', + /** ReceiveMessageWaitTimeSeconds */ + RECEIVE_MESSAGE_WAIT_TIME_SECONDS = 'ReceiveMessageWaitTimeSeconds', + /** RedrivePolicy */ + REDRIVE_POLICY = 'RedrivePolicy', + /** FifoQueue */ + FIFO_QUEUE = 'FifoQueue', + /** ContentBasedDeduplication */ + CONTENT_BASED_DEDUPLICATION = 'ContentBasedDeduplication', + /** KmsMasterKeyId */ + KMS_MASTER_KEY_ID = 'KmsMasterKeyId', + /** KmsDataKeyReusePeriodSeconds */ + KMS_DATA_KEY_REUSE_PERIOD_SECONDS = 'KmsDataKeyReusePeriodSeconds', + /** DeduplicationScope */ + DEDUPLICATION_SCOPE = 'DeduplicationScope', + /** FifoThroughputLimit */ + FIFO_THROUGHPUT_LIMIT = 'FifoThroughputLimit', +} + +/** + * A list of SqsAttributes, either a fixed value or mapped from the request. + */ +export class SqsMessageAttributeListMappingExpression { + /** + * Use a fixed value. + * + * @param attributeNames the value + */ + static fromAttributeList(attributeNames: Array) { + return new SqsMessageAttributeListMappingExpression(JSON.stringify(attributeNames)); + } + /** + * Use a mapping to set the value. + * + * @param mapping how to map the value + */ + static fromMapping(mapping: Mapping) { + return new SqsMessageAttributeListMappingExpression(mapping.expression); + } + /** + * @param mapping the mapping value + */ + private constructor(readonly mapping: string) { } +} + +/** + * Properties for the SQS-ReceiveMessage Integration. + */ +export interface SqsReceiveMessageIntegrationProps extends SqsIntegrationProps { + /** + * The Queue to read messages from. + */ + readonly queue: QueueMappingExpression; + /** + * The attributes to return. + * @default - undefined + */ + readonly attributeNames?: ArrayMappingExpression; + /** + * The maximum number of messages to receive. + * Valid values: 1 to 10. + * @default - 1 + */ + readonly maxNumberOfMessages?: StringMappingExpression; + /** + * The message attributes to return + * @default - undefined + */ + readonly messageAttributeNames?: SqsMessageAttributeListMappingExpression; + /** + * The token used for deduplication of ReceiveMessage calls. + * Only applicable to FIFO queues. + * @default - undefined + */ + readonly receiveRequestAttemptId?: StringMappingExpression; + /** + * The duration that the received messages are hidden from subsequent retrieve requests after + * being retrieved by a ReceiveMessage request. + * @default - undefined + */ + readonly visibilityTimeout?: DurationMappingExpression; + /** + * The duration for which the call waits for a message to arrive in the queue before returning. + * @default - undefined + */ + readonly waitTime?: DurationMappingExpression; +} + +/** + * An SQS-ReceiveMessage integration. + */ +export class SqsReceiveMessageIntegration + extends AwsServiceIntegration { + constructor(props: SqsReceiveMessageIntegrationProps) { + super(props); + } + bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { + const role = this.props.role ?? new Role(options.scope, 'Role', { + assumedBy: new ServicePrincipal('apigateway.amazonaws.com'), + }); + return { + type: this.integrationType, + subtype: HttpIntegrationSubtype.SQS_RECEIVEMESSAGE, + payloadFormatVersion: this.payloadFormatVersion, + credentials: IntegrationCredentials.fromRole(role), + requestParameters: { + QueueUrl: this.props.queue.mapping, + AttributeNames: this.props.attributeNames?.mapping, + MaxNumberOfMessages: this.props.maxNumberOfMessages?.mapping, + MessageAttributeNames: this.props.messageAttributeNames?.mapping, + ReceiveRequestAttemptId: this.props.receiveRequestAttemptId?.mapping, + VisibilityTimeout: this.props.visibilityTimeout?.mapping, + WaitTimeSeconds: this.props.waitTime?.mapping, + Region: this.props.region, + }, + }; + } +} + +/** + * Properties for an SQS-DeleteMessage integration. + */ +export interface SqsDeleteMessageIntegrationProps extends SqsIntegrationProps { + /** + * The receipt handle associated with the message to delete. + */ + readonly receiptHandle: StringMappingExpression; +} + +/** + * An SQS-DeleteMessage integration. + */ +export class SqsDeleteMessageIntegration extends AwsServiceIntegration { + constructor(props: SqsDeleteMessageIntegrationProps) { + super(props); + } + bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { + const role = this.props.role ?? new Role(options.scope, 'Role', { + assumedBy: new ServicePrincipal('apigateway.amazonaws.com'), + }); + return { + type: this.integrationType, + subtype: HttpIntegrationSubtype.SQS_DELETEMESSAGE, + payloadFormatVersion: this.payloadFormatVersion, + credentials: IntegrationCredentials.fromRole(role), + requestParameters: { + QueueUrl: this.props.queue.mapping, + ReceiptHandle: this.props.receiptHandle.mapping, + Region: this.props.region, + }, + }; + } +} + +/** + * Properties for the SQS-PurgeQueue integration. + */ +export interface SqsPurgeQueueIntegrationProps extends SqsIntegrationProps { +} + +/** + * An SQS-PurgeQueue integration. + */ +export class SqsPurgeQueueIntegration + extends AwsServiceIntegration { + constructor(props: SqsPurgeQueueIntegrationProps) { + super(props); + } + bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { + const role = this.props.role ?? new Role(options.scope, 'Role', { + assumedBy: new ServicePrincipal('apigateway.amazonaws.com'), + }); + return { + type: HttpIntegrationType.LAMBDA_PROXY, + subtype: HttpIntegrationSubtype.SQS_PURGEQUEUE, + payloadFormatVersion: PayloadFormatVersion.VERSION_1_0, + credentials: IntegrationCredentials.fromRole(role), + requestParameters: { + QueueUrl: this.props.queue.mapping, + Region: this.props.region, + }, + }; + } +} + +/** + * Properties for a Kinesis-PutRecord integration. + */ +export interface KinesisPutRecordIntegrationProps extends AwsServiceIntegrationProps { + /** + * The name of the stream to put the data record into. + */ + readonly stream: StreamMappingExpression; + /** + * The data blob to put into the record, which is base64-encoded when the blob is serialized. + */ + readonly data: StringMappingExpression; + /** + * Determines which shard in the stream the data record is assigned to. + */ + readonly partitionKey: StringMappingExpression; + /** + * Guarantees strictly increasing sequence numbers, for puts from the same client and to the same + * partition key. + * @default - undefined + */ + readonly sequenceNumberForOrdering?: StringMappingExpression; + /** + * The hash value used to explicitly determine the shard the data record is assigned to by + * overriding the partition key hash. + * @default - undefined + */ + readonly explicitHashKey?: StringMappingExpression; +} + +/** + * A Kinesis-PutRecord integration. + */ +export class KinesisPutRecordIntegration + extends AwsServiceIntegration { + constructor(props: KinesisPutRecordIntegrationProps) { + super(props); + } + bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { + const role = this.props.role ?? new Role(options.scope, 'Role', { + assumedBy: new ServicePrincipal('apigateway.amazonaws.com'), + }); + return { + type: HttpIntegrationType.LAMBDA_PROXY, + subtype: HttpIntegrationSubtype.KINESIS_PUTRECORD, + payloadFormatVersion: PayloadFormatVersion.VERSION_1_0, + credentials: IntegrationCredentials.fromRole(role), + requestParameters: { + StreamName: this.props.stream.mapping, + Data: this.props.data.mapping, + PartitionKey: this.props.partitionKey.mapping, + SequenceNumberForOrdering: this.props.sequenceNumberForOrdering?.mapping, + ExplicitHashKey: this.props.explicitHashKey?.mapping, + Region: this.props.region, + }, + }; + } +} + +/** + * Properties for a StepFunctions-StartExecution or StepFunctions-StartSyncExecution integration. + */ +export interface StepFunctionsStartExecutionIntegrationProps extends AwsServiceIntegrationProps { + /** + * The StateMachine to execute. + */ + readonly stateMachine: StateMachineMappingExpression; + /** + * The name of the execution. + * @default - undefined + */ + readonly name?: StringMappingExpression; + /** + * The string that contains the JSON input data for the execution. + * @default - no input + */ + readonly input?: StringMappingExpression; + /** + * Passes the AWS X-Ray trace header. + * @default - undefined + */ + readonly traceHeader?: StringMappingExpression; +} + +/** + * A StepFunctions-StartExecution integration. + */ +export class StepFunctionsStartExecutionIntegration + extends AwsServiceIntegration { + constructor(props: StepFunctionsStartExecutionIntegrationProps) { + super(props); + } + bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { + const role = this.props.role ?? new Role(options.scope, 'Role', { + assumedBy: new ServicePrincipal('apigateway.amazonaws.com'), + }); + return { + type: HttpIntegrationType.LAMBDA_PROXY, + subtype: HttpIntegrationSubtype.STEPFUNCTIONS_STARTEXECUTION, + payloadFormatVersion: PayloadFormatVersion.VERSION_1_0, + credentials: IntegrationCredentials.fromRole(role), + requestParameters: { + StateMachineArn: this.props.stateMachine.mapping, + Name: this.props.name?.mapping, + Input: this.props.input?.mapping, + Region: this.props.region, + }, + }; + } +} + +/** + * A StepFunctions-StartSyncExecution integration. + */ +export class StepFunctionsStartSyncExecutionIntegration + extends AwsServiceIntegration { + constructor(props: StepFunctionsStartExecutionIntegrationProps) { + super(props); + } + bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { + const role = this.props.role ?? new Role(options.scope, 'Role', { + assumedBy: new ServicePrincipal('apigateway.amazonaws.com'), + }); + return { + type: this.integrationType, + subtype: HttpIntegrationSubtype.STEPFUNCTIONS_STARTSYNCEXECUTION, + payloadFormatVersion: this.payloadFormatVersion, + credentials: IntegrationCredentials.fromRole(role), + requestParameters: { + StateMachineArn: this.props.stateMachine.mapping, + Name: this.props.name?.mapping, + Input: this.props.input?.mapping, + TraceHeader: this.props.traceHeader?.mapping, + Region: this.props.region, + }, + }; + } +} + +/** + * Properties for a StepFunctions-StopExecution integration. + */ +export interface StepFunctionsStopExecutionIntegrationProps extends AwsServiceIntegrationProps { + /** + * The Amazon Resource Name (ARN) of the execution to stop. + */ + readonly executionArn: StringMappingExpression; + /** + * A more detailed explanation of the cause of the failure. + * @default - undefined + */ + readonly cause?: StringMappingExpression; + /** + * The error code of the failure. + * @default - undefined + */ + readonly error?: StringMappingExpression; +} + +/** + * A StepFunctions-StopExecution integration. + */ +export class StepFunctionsStopExecutionIntegration + extends AwsServiceIntegration { + constructor(props: StepFunctionsStopExecutionIntegrationProps) { + super(props); + } + bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { + const role = this.props.role ?? new Role(options.scope, 'Role', { + assumedBy: new ServicePrincipal('apigateway.amazonaws.com'), + }); + return { + type: this.integrationType, + subtype: HttpIntegrationSubtype.STEPFUNCTIONS_STOPEXECUTION, + payloadFormatVersion: this.payloadFormatVersion, + credentials: IntegrationCredentials.fromRole(role), + requestParameters: { + ExecutionArn: this.props.executionArn.mapping, + Cause: this.props.cause?.mapping, + Error: this.props.error?.mapping, + Region: this.props.region, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/index.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/index.ts index 8e0598975f8cb..08105eba5329b 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/index.ts @@ -1,6 +1,8 @@ export * from './base-types'; export * from './alb'; +export * from './aws-proxy'; export * from './nlb'; export * from './service-discovery'; export * from './http-proxy'; export * from './lambda'; +export * from './mapping-expression'; diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/mapping-expression.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/mapping-expression.ts new file mode 100644 index 0000000000000..03de019466760 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/mapping-expression.ts @@ -0,0 +1,391 @@ +import { IEventBus } from '@aws-cdk/aws-events'; +import { IStream } from '@aws-cdk/aws-kinesis'; +import { IQueue } from '@aws-cdk/aws-sqs'; +import { IStateMachine } from '@aws-cdk/aws-stepfunctions'; +import { Duration } from '@aws-cdk/core'; + +/** Represents a mapping from the request onto the parameters for an integration */ +export class Mapping { + /** + * Use a value from the body. + * + * @param path a JSON path-like expression to select a value from the request + * @default - use the whole request body + */ + static fromRequestBody(path?: string) { + return new Mapping(`$request.body${path ? `.${path}` : ''}`); + } + /** + * Use a value from a the request header. + * + * @param headerName the header containing the mapped value + */ + static fromRequestHeader(headerName: string) { + return new Mapping(`$request.header.${headerName}`); + } + /** + * Use a value from the querystring. + * + * @param queryParam the name of the query parameter + */ + static fromQueryParam(queryParam: string) { + return new Mapping(`$request.querystring.${queryParam}`); + } + /** + * Use a value from the path. + * + * @param pathParam the path parameter containing the mapped value + */ + static fromRequestPath(pathParam: string) { + return new Mapping(`$request.path.${pathParam}`); + } + /** + * Use a context variable. + * + * @param variable the name of the context variable + */ + static fromContextVariable(variable: string) { + return new Mapping(`$context.${variable}`); + } + /** + * Use a state variable. + * + * @param variable the name of the state variable + */ + static fromStateVariable(variable: string) { + return new Mapping(`$stageVariables.${variable}`); + } + /** + * Supply a custom expression. + * + * @param expression the expression to map + */ + static fromCustomExpression(expression: string) { + return new Mapping(expression); + } + /** + * @param expression the value of the mapping expression + */ + private constructor(readonly expression: string) { } +} + +/** + * A string-valued property, either from a fixed value or mapped from the request. + */ +export class StringMappingExpression { + /** + * Use a fixed value. + * + * @param value the value + */ + static fromValue(value: string) { + return new StringMappingExpression(value); + } + /** + * Use a mapping to set the value. + * + * @param mapping how to map the value + */ + static fromMapping(mapping: Mapping) { + return new StringMappingExpression(mapping.expression); + } + /** + * @param mapping the mapping value + */ + private constructor(readonly mapping: string) { } +} + +/** + * A number-valued property, either from a fixed value or mapped from the request. + */ +export class NumberMappingExpression { + /** + * Use a fixed value. + * + * @param value the fixed value + */ + static fromValue(value: number) { + return new NumberMappingExpression(value.toString(10)); + } + /** + * Use a mapping to set the value. + * + * @param mapping how to map the value + */ + static fromMapping(mapping: Mapping) { + return new NumberMappingExpression(mapping.expression); + } + /** + * @param mapping the mapping value + */ + private constructor(readonly mapping: string) { } +} + +/** + * An Array-valued property, either from a fixed value or mapped from the request. + */ +export class ArrayMappingExpression { + /** + * Use a fixed value. + * + * @param value the fixed value + */ + static fromValue(value: Array) { + return new ArrayMappingExpression(JSON.stringify(value)); + } + /** + * Use a mapping to set the value. + * + * @param mapping how to map the value + */ + static fromMapping(mapping: Mapping) { + return new ArrayMappingExpression(mapping.expression); + } + /** + * @param mapping the mapping value + */ + private constructor(readonly mapping: string) { } +} + +/** + * An EventBus property, either from a fixed value or mapped from the request. + */ +export class EventBusMappingExpression { + /** + * Use a fixed value. + * + * @param eventBus the fixed value + */ + static fromEventBus(eventBus: IEventBus) { + return new EventBusMappingExpression(eventBus.eventBusName); + } + /** + * Use a mapping to set the value. + * + * @param mapping how to map the value + */ + static fromMapping(mapping: Mapping) { + return new EventBusMappingExpression(mapping.expression); + } + /** + * @param mapping the mapping value + */ + private constructor(readonly mapping: string) { } +} + +/** + * A Date-valued property, either a fixed value or mapped from the request. + */ +export class DateMappingExpression { + /** + * Use a fixed value. + * + * @param date the fixed value + */ + static fromDate(date: Date) { + return new DateMappingExpression(date.toISOString()); + } + /** + * Use a mapping to set the value. + * + * @param mapping how to map the value + */ + static fromMapping(mapping: Mapping) { + return new DateMappingExpression(mapping.expression); + } + /** + * @param mapping the mapping value + */ + private constructor(readonly mapping: string) { } +} + +/** + * A Queue-value property, either from a fixed value or mapped from the request. + */ +export class QueueMappingExpression { + /** + * Use a fixed value. + * + * @param queue the fixed value + */ + static fromQueue(queue: IQueue) { + return new QueueMappingExpression(queue.queueUrl); + } + /** + * Use a mapping to set the value. + * + * @param mapping how to map the value + */ + static fromMapping(mapping: Mapping) { + return new QueueMappingExpression(mapping.expression); + } + /** + * @param mapping the mapping value + */ + private constructor(readonly mapping: string) { } +} + +/** An SQS Message Attribute definition */ +export interface ISqsAttribute { + /** The object representing the message attribute */ + readonly json: object; +} + +/** Represents a String-valued message attribute */ +export class SqsStringAttribute implements ISqsAttribute { + readonly json: object; + /** + * @param name the attribute name + * @param value the attribute value + */ + constructor(name: string, value: string) { + this.json = { + [name]: { + DataType: 'String', + StringValue: value, + }, + }; + } +} + +/** Represents a numeric message attribute */ +export class SqsNumberAttribute implements ISqsAttribute { + /** The object representing the message attribute */ + readonly json: object; + /** + * @param name the attribute name + * @param value the attribute value + */ + constructor(name: string, value: number) { + this.json = { + [name]: { + DataType: 'Number', + StringValue: value.toString(10), + }, + }; + } +} + +/** Represents a binary message attribute */ +export class SqsBinaryAttribute implements ISqsAttribute { + /** The object representing the message attribute */ + readonly json: object; + /** + * @param name the attribute name + * @param value the base64 encoded attribute value + */ + constructor(name: string, value: string) { + this.json = { + [name]: { + DataType: 'Binary', + BinaryValue: value, + }, + }; + } +} + +/** + * Maps a list of message attributes, either from static values or mapped from the request. + */ +export class SqsAttributeListMappingExpression { + /** + * Use a fixed value + * + * @param list the fixed value + */ + static fromSqsAttributeList(list: Array) { + return new SqsAttributeListMappingExpression(JSON.stringify(list.reduce((acc, attribute) => { + return Object.assign(acc, attribute.json); + }, {}))); + } + /** + * Use a mapping to set the value. + * + * @param mapping how to map the value + */ + static fromMapping(mapping: Mapping) { + return new SqsAttributeListMappingExpression(mapping.expression); + } + /** + * @param mapping the mapping value + */ + private constructor(readonly mapping: string) { } +} + +/** + * A Duration-valued property, either a fixed value or mapped from the request. + */ +export class DurationMappingExpression { + /** + * Use a fixed value. + * + * @param duration the fixed value + */ + static fromDuration(duration: Duration) { + return new DurationMappingExpression(duration.toSeconds().toString()); + } + /** + * Use a mapping to set the value. + * + * @param mapping how to map the value + */ + static fromMapping(mapping: Mapping) { + return new DurationMappingExpression(mapping.expression); + } + /** + * @param mapping the mapping value + */ + private constructor(readonly mapping: string) { } +} + +/** + * A Stream-value property, either from a fixed value or mapped from the request. + */ +export class StreamMappingExpression { + /** + * Use a fixed value. + * + * @param stream the fixed value + */ + static fromStream(stream: IStream) { + return new StreamMappingExpression(stream.streamName); + } + /** + * Use a mapping to set the value. + * + * @param mapping how to map the value + */ + static fromMapping(mapping: Mapping) { + return new StreamMappingExpression(mapping.expression); + } + /** + * @param mapping the mapping value + */ + private constructor(readonly mapping: string) { } +} + +/** + * A StateMachine-value property, either from a fixed value or mapped from the request. + */ +export class StateMachineMappingExpression { + /** + * Use a fixed value. + * + * @param stateMachine the fixed value + */ + static fromStateMachine(stateMachine: IStateMachine) { + return new StateMachineMappingExpression(stateMachine.stateMachineArn); + } + /** + * Use a mapping to set the value. + * + * @param mapping how to map the value + */ + static fromMapping(mapping: Mapping) { + return new StateMachineMappingExpression(mapping.expression); + } + /** + * @param mapping the mapping value + */ + private constructor(readonly mapping: string) { } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json index f857a94bc93f4..c78955d1768f0 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json @@ -71,7 +71,10 @@ }, "license": "Apache-2.0", "devDependencies": { + "@aws-cdk/assert-internal": "0.0.0", "@aws-cdk/assertions": "0.0.0", + "@aws-cdk/aws-events-targets": "0.0.0", + "@aws-cdk/aws-logs": "0.0.0", "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", @@ -81,9 +84,13 @@ "@aws-cdk/aws-apigatewayv2": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", + "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kinesis": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-servicediscovery": "0.0.0", + "@aws-cdk/aws-sqs": "0.0.0", + "@aws-cdk/aws-stepfunctions": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, @@ -91,9 +98,13 @@ "@aws-cdk/aws-apigatewayv2": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", + "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kinesis": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-servicediscovery": "0.0.0", + "@aws-cdk/aws-sqs": "0.0.0", + "@aws-cdk/aws-stepfunctions": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, @@ -107,5 +118,10 @@ }, "publishConfig": { "tag": "latest" + }, + "awslint": { + "exclude": [ + "duration-prop-type:@aws-cdk/aws-apigatewayv2-integrations.SqsReceiveMessageIntegrationProps.visibilityTimeout" + ] } } diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/event-bridge.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/event-bridge.test.ts new file mode 100644 index 0000000000000..22285a7dacde3 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/event-bridge.test.ts @@ -0,0 +1,77 @@ +import '@aws-cdk/assert-internal/jest'; +import { HttpApi, HttpRoute, HttpRouteKey } from '@aws-cdk/aws-apigatewayv2'; +import { EventBus } from '@aws-cdk/aws-events'; +import { Role } from '@aws-cdk/aws-iam'; +import { Stack } from '@aws-cdk/core'; +import { EventBridgePutEventsIntegration } from '../../lib/http/aws-proxy'; +import { EventBusMappingExpression, StringMappingExpression } from '../../lib/http/mapping-expression'; + +describe('EventBridge PutEvents Integration', () => { + test('basic integration', () => { + const stack = new Stack(); + const api = new HttpApi(stack, 'API'); + const role = Role.fromRoleArn(stack, 'TestRole', 'arn:aws:iam::123456789012:role/test'); + new HttpRoute(stack, 'Route', { + httpApi: api, + integration: new EventBridgePutEventsIntegration({ + detail: StringMappingExpression.fromValue('detail'), + detailType: StringMappingExpression.fromValue('type'), + source: StringMappingExpression.fromValue('source'), + role, + }), + routeKey: HttpRouteKey.with('/event'), + }); + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'AWS_PROXY', + IntegrationSubtype: 'EventBridge-PutEvents', + PayloadFormatVersion: '1.0', + CredentialsArn: 'arn:aws:iam::123456789012:role/test', + RequestParameters: { + Detail: 'detail', + DetailType: 'type', + Source: 'source', + }, + }); + }); + test('full integration', () => { + const stack = new Stack(); + const api = new HttpApi(stack, 'API'); + const role = Role.fromRoleArn(stack, 'TestRole', 'arn:aws:iam::123456789012:role/test'); + const eventBus = EventBus.fromEventBusArn(stack, + 'EventBus', + 'arn:aws:events:eu-west-1:123456789012:event-bus/different', + ); + new HttpRoute(stack, 'Route', { + httpApi: api, + integration: new EventBridgePutEventsIntegration({ + detail: StringMappingExpression.fromValue('detail'), + detailType: StringMappingExpression.fromValue('detail-type'), + source: StringMappingExpression.fromValue('source'), + role, + eventBus: EventBusMappingExpression.fromEventBus(eventBus), + region: 'eu-west-1', + resources: StringMappingExpression.fromValue('["arn:aws:s3:::bucket"]'), + time: StringMappingExpression.fromValue('2021-07-14T20:18:15Z'), + traceHeader: StringMappingExpression.fromValue('x-trace-header'), + }), + routeKey: HttpRouteKey.with('/event'), + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'AWS_PROXY', + IntegrationSubtype: 'EventBridge-PutEvents', + PayloadFormatVersion: '1.0', + CredentialsArn: 'arn:aws:iam::123456789012:role/test', + RequestParameters: { + Detail: 'detail', + DetailType: 'detail-type', + Source: 'source', + EventBusName: 'different', + Region: 'eu-west-1', + Resources: '["arn:aws:s3:::bucket"]', + Time: '2021-07-14T20:18:15Z', + TraceHeader: 'x-trace-header', + }, + }); + }); +}); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.alb.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.alb.expected.json index b9f5d96ff4656..e697d13ea899b 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.alb.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.alb.expected.json @@ -95,15 +95,15 @@ "VPCPublicSubnet1NATGatewayE0556630": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, "AllocationId": { "Fn::GetAtt": [ "VPCPublicSubnet1EIP6AD938E8", "AllocationId" ] }, - "SubnetId": { - "Ref": "VPCPublicSubnet1SubnetB4246D30" - }, "Tags": [ { "Key": "Name", @@ -192,15 +192,15 @@ "VPCPublicSubnet2NATGateway3C070193": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, "AllocationId": { "Fn::GetAtt": [ "VPCPublicSubnet2EIP4947BC00", "AllocationId" ] }, - "SubnetId": { - "Ref": "VPCPublicSubnet2Subnet74179F39" - }, "Tags": [ { "Key": "Name", @@ -289,15 +289,15 @@ "VPCPublicSubnet3NATGatewayD3048F5C": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + }, "AllocationId": { "Fn::GetAtt": [ "VPCPublicSubnet3EIPAD4BC883", "AllocationId" ] }, - "SubnetId": { - "Ref": "VPCPublicSubnet3Subnet631C5E25" - }, "Tags": [ { "Key": "Name", diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.event-bridge.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.event-bridge.expected.json new file mode 100644 index 0000000000000..a42ab64d06935 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.event-bridge.expected.json @@ -0,0 +1,358 @@ +{ + "Resources": { + "IntegrationApi17ACB35A": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "IntegrationApi", + "ProtocolType": "HTTP" + } + }, + "IntegrationApiDefaultStageBE4B2B3D": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "IntegrationApi17ACB35A" + }, + "StageName": "$default", + "AutoDeploy": true + } + }, + "IntegrationApiANYputEventHttpIntegrationd211065a4e759a9b1c015c09b9b0995306CD51DF": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "IntegrationApi17ACB35A" + }, + "IntegrationType": "AWS_PROXY", + "CredentialsArn": { + "Fn::GetAtt": [ + "PutEventsFA73D0E4", + "Arn" + ] + }, + "IntegrationSubtype": "EventBridge-PutEvents", + "PayloadFormatVersion": "1.0", + "RequestParameters": { + "Detail": "$request.body", + "DetailType": "test", + "Source": "integ-event-bridge", + "EventBusName": "default", + "Resources": "$request.body.resources" + } + } + }, + "IntegrationApiANYputEvent7FEA02CC": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "IntegrationApi17ACB35A" + }, + "RouteKey": "ANY /putEvent", + "AuthorizationType": "NONE", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "IntegrationApiANYputEventHttpIntegrationd211065a4e759a9b1c015c09b9b0995306CD51DF" + } + ] + ] + } + } + }, + "PutEventsFA73D0E4": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PutEventsDefaultPolicy50296286": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "events:PutEvents", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":events:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":event-bus/default" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PutEventsDefaultPolicy50296286", + "Roles": [ + { + "Ref": "PutEventsFA73D0E4" + } + ] + } + }, + "VerificationBFE7ED42": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "LogGroupName": "/aws/events/integ-event-bridge", + "RetentionInDays": 1 + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "LogRule658B2F59": { + "Type": "AWS::Events::Rule", + "Properties": { + "EventPattern": { + "source": [ + "integ-event-bridge" + ], + "detail-type": [ + "test" + ] + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:", + { + "Ref": "VerificationBFE7ED42" + } + ] + ] + }, + "Id": "Target0" + } + ] + } + }, + "EventsLogGroupPolicyintegeventbridgeLogRule0021FD38CustomResourcePolicyA13FA0D3": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "logs:PutResourcePolicy", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "logs:DeleteResourcePolicy", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "EventsLogGroupPolicyintegeventbridgeLogRule0021FD38CustomResourcePolicyA13FA0D3", + "Roles": [ + { + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + } + ] + } + }, + "EventsLogGroupPolicyintegeventbridgeLogRule0021FD385C6BF83E": { + "Type": "Custom::CloudwatchLogResourcePolicy", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd22872D164C4C", + "Arn" + ] + }, + "Create": { + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"integeventbridgeEventsLogGroupPolicyintegeventbridgeLogRule0021FD38759A9E44\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"events.amazonaws.com\\\"},\\\"Resource\\\":\\\"", + { + "Fn::GetAtt": [ + "VerificationBFE7ED42", + "Arn" + ] + }, + "\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"EventsLogGroupPolicyintegeventbridgeLogRule0021FD38\"}}" + ] + ] + }, + "Update": { + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"integeventbridgeEventsLogGroupPolicyintegeventbridgeLogRule0021FD38759A9E44\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"events.amazonaws.com\\\"},\\\"Resource\\\":\\\"", + { + "Fn::GetAtt": [ + "VerificationBFE7ED42", + "Arn" + ] + }, + "\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"EventsLogGroupPolicyintegeventbridgeLogRule0021FD38\"}}" + ] + ] + }, + "Delete": "{\"service\":\"CloudWatchLogs\",\"action\":\"deleteResourcePolicy\",\"parameters\":{\"policyName\":\"integeventbridgeEventsLogGroupPolicyintegeventbridgeLogRule0021FD38759A9E44\"},\"ignoreErrorCodesMatching\":\"400\"}", + "InstallLatestAwsSdk": true + }, + "DependsOn": [ + "EventsLogGroupPolicyintegeventbridgeLogRule0021FD38CustomResourcePolicyA13FA0D3" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2": { + "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" + ] + ] + } + ] + } + }, + "AWS679f53fac002430cb0da5b7982bd22872D164C4C": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4S3BucketB17E5ABD" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4S3VersionKey77778F6A" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4S3VersionKey77778F6A" + } + ] + } + ] + } + ] + ] + } + }, + "Role": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs12.x", + "Timeout": 120 + }, + "DependsOn": [ + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + ] + } + }, + "Parameters": { + "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4S3BucketB17E5ABD": { + "Type": "String", + "Description": "S3 bucket for asset \"5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4\"" + }, + "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4S3VersionKey77778F6A": { + "Type": "String", + "Description": "S3 key for asset version \"5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4\"" + }, + "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4ArtifactHash580E429C": { + "Type": "String", + "Description": "Artifact hash for asset \"5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4\"" + } + }, + "Outputs": { + "ApiGatewayUrl": { + "Value": { + "Fn::GetAtt": [ + "IntegrationApi17ACB35A", + "ApiEndpoint" + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.event-bridge.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.event-bridge.ts new file mode 100644 index 0000000000000..0f21b89a243a5 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.event-bridge.ts @@ -0,0 +1,57 @@ +import { HttpApi } from '@aws-cdk/aws-apigatewayv2'; +import { EventBus, Rule } from '@aws-cdk/aws-events'; +import { CloudWatchLogGroup } from '@aws-cdk/aws-events-targets'; +import { PolicyStatement, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; +import { LogGroup, RetentionDays } from '@aws-cdk/aws-logs'; +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { EventBridgePutEventsIntegration } from '../../lib/http/aws-proxy'; +import { ArrayMappingExpression, EventBusMappingExpression, Mapping, StringMappingExpression } from '../../lib/http/mapping-expression'; + +/* + * Stack verification steps: + * * Deploy the stack and wait for the API URL to be displayed. + * * send a POST request to /putEvent. + * * verify that the request has arrived at the log group /aws/events/integ-event-bridge + */ + +const app = new App(); +const stack = new Stack(app, 'integ-event-bridge'); +const httpApi = new HttpApi(stack, 'IntegrationApi'); +const putEventsRole = new Role(stack, 'PutEvents', { + assumedBy: new ServicePrincipal('apigateway.amazonaws.com'), +}); + +const eventBus = EventBus.fromEventBusName(stack, 'Default', 'default'); +httpApi.addRoutes({ + path: '/putEvent', + integration: new EventBridgePutEventsIntegration({ + detail: StringMappingExpression.fromValue('$request.body'), + detailType: StringMappingExpression.fromValue('test'), + source: StringMappingExpression.fromValue('integ-event-bridge'), + eventBus: EventBusMappingExpression.fromEventBus(eventBus), + role: putEventsRole, + resources: ArrayMappingExpression.fromMapping(Mapping.fromRequestBody('resources')), + }), +}); + +putEventsRole.addToPrincipalPolicy(new PolicyStatement({ + actions: ['events:PutEvents'], + resources: [eventBus.eventBusArn], +})); + +const logGroup = new LogGroup(stack, 'Verification', { + logGroupName: '/aws/events/integ-event-bridge', + retention: RetentionDays.ONE_DAY, +}); + +new Rule(stack, 'LogRule', { + eventPattern: { + source: ['integ-event-bridge'], + detailType: ['test'], + }, + targets: [new CloudWatchLogGroup(logGroup)], +}); + +new CfnOutput(stack, 'ApiGatewayUrl', { + value: httpApi.apiEndpoint, +}); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.kinesis.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.kinesis.expected.json new file mode 100644 index 0000000000000..134a0fddc9a9b --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.kinesis.expected.json @@ -0,0 +1,165 @@ +{ + "Resources": { + "IntegKinesisApi890BF27F": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "IntegKinesisApi", + "ProtocolType": "HTTP" + } + }, + "IntegKinesisApiDefaultStage245E3852": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "IntegKinesisApi890BF27F" + }, + "StageName": "$default", + "AutoDeploy": true + } + }, + "IntegKinesisApiANYHttpIntegrationc304c3d15ebb729a043fc95e653032645012054C": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "IntegKinesisApi890BF27F" + }, + "IntegrationType": "AWS_PROXY", + "CredentialsArn": { + "Fn::GetAtt": [ + "Role1ABCC5F0", + "Arn" + ] + }, + "IntegrationSubtype": "Kinesis-PutRecord", + "PayloadFormatVersion": "1.0", + "RequestParameters": { + "StreamName": { + "Ref": "Stream790BDEE4" + }, + "Data": "$request.body", + "PartitionKey": "1" + } + } + }, + "IntegKinesisApiANY248F7340": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "IntegKinesisApi890BF27F" + }, + "RouteKey": "ANY /", + "AuthorizationType": "NONE", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "IntegKinesisApiANYHttpIntegrationc304c3d15ebb729a043fc95e653032645012054C" + } + ] + ] + } + } + }, + "Stream790BDEE4": { + "Type": "AWS::Kinesis::Stream", + "Properties": { + "ShardCount": 1, + "RetentionPeriodHours": 24, + "StreamEncryption": { + "Fn::If": [ + "AwsCdkKinesisEncryptedStreamsUnsupportedRegions", + { + "Ref": "AWS::NoValue" + }, + { + "EncryptionType": "KMS", + "KeyId": "alias/aws/kinesis" + } + ] + } + } + }, + "Role1ABCC5F0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "RoleDefaultPolicy5FFB7DAB": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "kinesis:PutRecord", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Stream790BDEE4", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "RoleDefaultPolicy5FFB7DAB", + "Roles": [ + { + "Ref": "Role1ABCC5F0" + } + ] + } + } + }, + "Conditions": { + "AwsCdkKinesisEncryptedStreamsUnsupportedRegions": { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "cn-north-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "cn-northwest-1" + ] + } + ] + } + }, + "Outputs": { + "ApiUrl": { + "Value": { + "Fn::GetAtt": [ + "IntegKinesisApi890BF27F", + "ApiEndpoint" + ] + } + }, + "StreamName": { + "Value": { + "Ref": "Stream790BDEE4" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.kinesis.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.kinesis.ts new file mode 100644 index 0000000000000..f4c5078a2a596 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.kinesis.ts @@ -0,0 +1,40 @@ +import { HttpApi } from '@aws-cdk/aws-apigatewayv2'; +import { PolicyStatement, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; +import { Stream } from '@aws-cdk/aws-kinesis'; +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { KinesisPutRecordIntegration, Mapping, StreamMappingExpression, StringMappingExpression } from '../../lib'; + +// Stack verification steps +// deploy the stack and find the api URL and the kinesis stream name +// make a POST request to the URL, with a message body +// verify that the message has been written to the kinesis stream: +// aws kinesis get-shard-iterator --stream-name --shard-id 0 --shard-iterator-type TRIM_HORIZON +// aws kinesis get-records --shard-iterator +// The message will be base64 encoded in the Data attribute of the record +const app = new App(); +const stack = new Stack(app, 'integ-kinesis'); +const httpApi = new HttpApi(stack, 'IntegKinesisApi'); +const stream = new Stream(stack, 'Stream'); +const role = new Role(stack, 'Role', { + assumedBy: new ServicePrincipal('apigateway.amazonaws.com'), +}); +role.addToPrincipalPolicy(new PolicyStatement({ + actions: ['kinesis:PutRecord'], + resources: [stream.streamArn], +})); +httpApi.addRoutes({ + path: '/', + integration: new KinesisPutRecordIntegration({ + data: StringMappingExpression.fromMapping(Mapping.fromRequestBody()), + stream: StreamMappingExpression.fromStream(stream), + partitionKey: StringMappingExpression.fromValue('1'), + role, + }), +}); + +new CfnOutput(stack, 'ApiUrl', { + value: httpApi.apiEndpoint, +}); +new CfnOutput(stack, 'StreamName', { + value: stream.streamName, +}); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.nlb.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.nlb.expected.json index 0a3241cdc8139..7ddc5a5ccf266 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.nlb.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.nlb.expected.json @@ -95,15 +95,15 @@ "VPCPublicSubnet1NATGatewayE0556630": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, "AllocationId": { "Fn::GetAtt": [ "VPCPublicSubnet1EIP6AD938E8", "AllocationId" ] }, - "SubnetId": { - "Ref": "VPCPublicSubnet1SubnetB4246D30" - }, "Tags": [ { "Key": "Name", @@ -192,15 +192,15 @@ "VPCPublicSubnet2NATGateway3C070193": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, "AllocationId": { "Fn::GetAtt": [ "VPCPublicSubnet2EIP4947BC00", "AllocationId" ] }, - "SubnetId": { - "Ref": "VPCPublicSubnet2Subnet74179F39" - }, "Tags": [ { "Key": "Name", @@ -289,15 +289,15 @@ "VPCPublicSubnet3NATGatewayD3048F5C": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + }, "AllocationId": { "Fn::GetAtt": [ "VPCPublicSubnet3EIPAD4BC883", "AllocationId" ] }, - "SubnetId": { - "Ref": "VPCPublicSubnet3Subnet631C5E25" - }, "Tags": [ { "Key": "Name", diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.service-discovery.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.service-discovery.expected.json index 00e587f8ac85f..b6b1c8f856c32 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.service-discovery.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.service-discovery.expected.json @@ -95,15 +95,15 @@ "VPCPublicSubnet1NATGatewayE0556630": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, "AllocationId": { "Fn::GetAtt": [ "VPCPublicSubnet1EIP6AD938E8", "AllocationId" ] }, - "SubnetId": { - "Ref": "VPCPublicSubnet1SubnetB4246D30" - }, "Tags": [ { "Key": "Name", @@ -192,15 +192,15 @@ "VPCPublicSubnet2NATGateway3C070193": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, "AllocationId": { "Fn::GetAtt": [ "VPCPublicSubnet2EIP4947BC00", "AllocationId" ] }, - "SubnetId": { - "Ref": "VPCPublicSubnet2Subnet74179F39" - }, "Tags": [ { "Key": "Name", @@ -289,15 +289,15 @@ "VPCPublicSubnet3NATGatewayD3048F5C": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + }, "AllocationId": { "Fn::GetAtt": [ "VPCPublicSubnet3EIPAD4BC883", "AllocationId" ] }, - "SubnetId": { - "Ref": "VPCPublicSubnet3Subnet631C5E25" - }, "Tags": [ { "Key": "Name", diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.sqs.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.sqs.expected.json new file mode 100644 index 0000000000000..8742d0e974822 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.sqs.expected.json @@ -0,0 +1,257 @@ +{ + "Resources": { + "IntegrationApi17ACB35A": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "IntegrationApi", + "ProtocolType": "HTTP" + } + }, + "IntegrationApiDefaultStageBE4B2B3D": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "IntegrationApi17ACB35A" + }, + "StageName": "$default", + "AutoDeploy": true + } + }, + "IntegrationApiPOSTmessageHttpIntegratione815734e0bae601a490fc354f818edb53B58A0ED": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "IntegrationApi17ACB35A" + }, + "IntegrationType": "AWS_PROXY", + "CredentialsArn": { + "Fn::GetAtt": [ + "SQSRole496249D8", + "Arn" + ] + }, + "IntegrationSubtype": "SQS-SendMessage", + "PayloadFormatVersion": "1.0", + "RequestParameters": { + "QueueUrl": { + "Ref": "Queue4A7E3555" + }, + "MessageBody": "$request.body", + "DelaySeconds": "10", + "MessageAttributes": "{\"foo\":{\"DataType\":\"String\",\"StringValue\":\"bar\"}}" + } + } + }, + "IntegrationApiPOSTmessageC639435F": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "IntegrationApi17ACB35A" + }, + "RouteKey": "POST /message", + "AuthorizationType": "NONE", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "IntegrationApiPOSTmessageHttpIntegratione815734e0bae601a490fc354f818edb53B58A0ED" + } + ] + ] + } + } + }, + "IntegrationApiGETmessageHttpIntegration78ac6bbd3060bdd0aa49beb18eb95640D617AC9C": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "IntegrationApi17ACB35A" + }, + "IntegrationType": "AWS_PROXY", + "CredentialsArn": { + "Fn::GetAtt": [ + "SQSRole496249D8", + "Arn" + ] + }, + "IntegrationSubtype": "SQS-ReceiveMessage", + "PayloadFormatVersion": "1.0", + "RequestParameters": { + "QueueUrl": { + "Ref": "Queue4A7E3555" + }, + "AttributeNames": "[\"foo\"]", + "MessageAttributeNames": "[\"All\"]" + } + } + }, + "IntegrationApiGETmessageF2DC1C99": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "IntegrationApi17ACB35A" + }, + "RouteKey": "GET /message", + "AuthorizationType": "NONE", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "IntegrationApiGETmessageHttpIntegration78ac6bbd3060bdd0aa49beb18eb95640D617AC9C" + } + ] + ] + } + } + }, + "IntegrationApiDELETEmessagemessageIdHttpIntegrationaf3cbbc00ccd6e8a76d43b462b789a532366D023": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "IntegrationApi17ACB35A" + }, + "IntegrationType": "AWS_PROXY", + "CredentialsArn": { + "Fn::GetAtt": [ + "SQSRole496249D8", + "Arn" + ] + }, + "IntegrationSubtype": "SQS-DeleteMessage", + "PayloadFormatVersion": "1.0", + "RequestParameters": { + "QueueUrl": { + "Ref": "Queue4A7E3555" + }, + "ReceiptHandle": "$request.path.messageId" + } + } + }, + "IntegrationApiDELETEmessagemessageIdED3C4EC9": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "IntegrationApi17ACB35A" + }, + "RouteKey": "DELETE /message/{messageId+}", + "AuthorizationType": "NONE", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "IntegrationApiDELETEmessagemessageIdHttpIntegrationaf3cbbc00ccd6e8a76d43b462b789a532366D023" + } + ] + ] + } + } + }, + "IntegrationApiDELETEmessageHttpIntegrationa18f4f421437676e6c10b3913ea994eaB730BF70": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "IntegrationApi17ACB35A" + }, + "IntegrationType": "AWS_PROXY", + "CredentialsArn": { + "Fn::GetAtt": [ + "SQSRole496249D8", + "Arn" + ] + }, + "IntegrationSubtype": "SQS-PurgeQueue", + "PayloadFormatVersion": "1.0", + "RequestParameters": { + "QueueUrl": { + "Ref": "Queue4A7E3555" + } + } + } + }, + "IntegrationApiDELETEmessage40CC134C": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "IntegrationApi17ACB35A" + }, + "RouteKey": "DELETE /message", + "AuthorizationType": "NONE", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "IntegrationApiDELETEmessageHttpIntegrationa18f4f421437676e6c10b3913ea994eaB730BF70" + } + ] + ] + } + } + }, + "Queue4A7E3555": { + "Type": "AWS::SQS::Queue", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "SQSRole496249D8": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "SQSRoleDefaultPolicy25EA27EB": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:*", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Queue4A7E3555", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "SQSRoleDefaultPolicy25EA27EB", + "Roles": [ + { + "Ref": "SQSRole496249D8" + } + ] + } + } + }, + "Outputs": { + "ApiGatewayUrl": { + "Value": { + "Fn::GetAtt": [ + "IntegrationApi17ACB35A", + "ApiEndpoint" + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.sqs.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.sqs.ts new file mode 100644 index 0000000000000..b2677c5d9f22d --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.sqs.ts @@ -0,0 +1,80 @@ +import { HttpApi, HttpMethod } from '@aws-cdk/aws-apigatewayv2'; +import { PolicyStatement, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; +import { Queue } from '@aws-cdk/aws-sqs'; +import { App, CfnOutput, Duration, Stack } from '@aws-cdk/core'; +import { SqsMessageAttributeListMappingExpression, SqsMessageAttribute, SqsDeleteMessageIntegration, SqsPurgeQueueIntegration, SqsReceiveMessageIntegration, SqsSendMessageIntegration } from '../../lib/http/aws-proxy'; +import { ArrayMappingExpression, DurationMappingExpression, Mapping, QueueMappingExpression, SqsAttributeListMappingExpression, SqsStringAttribute, StringMappingExpression } from '../../lib/http/mapping-expression'; + +/* + * Verification steps: + * : Deploy the stack and locate the ApiGatewayUrl output + * : make a POST request to /message with a message body + * : Immediately make a GET request to /message; no messages should be returned + * : Wait 10 seconds and get from /message again; the message should be displayed; + * note the ReceiptHandle in the response. + * : make a DELETE request to /message/ + * : get messages; none should be present. + * : post a new message. + * : make a DELETE request to /message. + * : get messages; none should be present. + */ +const app = new App(); +const stack = new Stack(app, 'integ-sqs'); +const httpApi = new HttpApi(stack, 'IntegrationApi'); + +const queue = new Queue(stack, 'Queue'); +const queueMapping = QueueMappingExpression.fromQueue(queue); + +const role = new Role(stack, 'SQSRole', { + assumedBy: new ServicePrincipal('apigateway.amazonaws.com'), +}); +role.addToPrincipalPolicy(new PolicyStatement({ + actions: ['sqs:*'], + resources: [queue.queueArn], +})); + +httpApi.addRoutes({ + path: '/message', + methods: [HttpMethod.POST], + integration: new SqsSendMessageIntegration({ + role, + body: StringMappingExpression.fromMapping(Mapping.fromRequestBody()), + queue: queueMapping, + attributes: SqsAttributeListMappingExpression.fromSqsAttributeList([new SqsStringAttribute('foo', 'bar')]), + delay: DurationMappingExpression.fromDuration(Duration.seconds(10)), + }), +}); + +httpApi.addRoutes({ + path: '/message', + methods: [HttpMethod.GET], + integration: new SqsReceiveMessageIntegration({ + role, + queue: queueMapping, + attributeNames: ArrayMappingExpression.fromValue(['foo']), + messageAttributeNames: SqsMessageAttributeListMappingExpression.fromAttributeList([SqsMessageAttribute.ALL]), + }), +}); + +httpApi.addRoutes({ + path: '/message/{messageId+}', + methods: [HttpMethod.DELETE], + integration: new SqsDeleteMessageIntegration({ + role, + queue: queueMapping, + receiptHandle: StringMappingExpression.fromMapping(Mapping.fromRequestPath('messageId')), + }), +}); + +httpApi.addRoutes({ + path: '/message', + methods: [HttpMethod.DELETE], + integration: new SqsPurgeQueueIntegration({ + role, + queue: queueMapping, + }), +}); + +new CfnOutput(stack, 'ApiGatewayUrl', { + value: httpApi.apiEndpoint, +}); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.step-functions.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.step-functions.expected.json new file mode 100644 index 0000000000000..6fabe7c491d60 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.step-functions.expected.json @@ -0,0 +1,330 @@ +{ + "Resources": { + "StateMachineRoleB840431D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, + "DefinitionString": "{\"StartAt\":\"Waiter\",\"States\":{\"Waiter\":{\"Type\":\"Wait\",\"Seconds\":30,\"End\":true}}}" + }, + "DependsOn": [ + "StateMachineRoleB840431D" + ] + }, + "StateMachineRole78EC082E": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachineRoleDefaultPolicy433448B5": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "StateMachine2E01A3A5" + } + }, + { + "Action": "states:StopExecution", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":states:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":execution:", + { + "Fn::Select": [ + 6, + { + "Fn::Split": [ + ":", + { + "Ref": "StateMachine2E01A3A5" + } + ] + } + ] + }, + ":*" + ] + ] + } + }, + { + "Action": "states:StartSyncExecution", + "Effect": "Allow", + "Resource": { + "Ref": "ExpressEE4D4F3B" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachineRoleDefaultPolicy433448B5", + "Roles": [ + { + "Ref": "StateMachineRole78EC082E" + } + ] + } + }, + "ExpressRole9F822A77": { + "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" + } + } + }, + "ExpressEE4D4F3B": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "ExpressRole9F822A77", + "Arn" + ] + }, + "DefinitionString": "{\"StartAt\":\"ExpressWait\",\"States\":{\"ExpressWait\":{\"Type\":\"Wait\",\"Seconds\":5,\"End\":true}}}", + "StateMachineType": "EXPRESS" + }, + "DependsOn": [ + "ExpressRole9F822A77" + ] + }, + "HttpApiF5A9A8A7": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "HttpApi", + "ProtocolType": "HTTP" + } + }, + "HttpApiDefaultStage3EEB07D6": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "HttpApiF5A9A8A7" + }, + "StageName": "$default", + "AutoDeploy": true + } + }, + "HttpApiPOSTasyncHttpIntegrationd384b5f9186315e0976ef6d3916dc4f68D037EA2": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "HttpApiF5A9A8A7" + }, + "IntegrationType": "AWS_PROXY", + "CredentialsArn": { + "Fn::GetAtt": [ + "StateMachineRole78EC082E", + "Arn" + ] + }, + "IntegrationSubtype": "StepFunctions-StartExecution", + "PayloadFormatVersion": "1.0", + "RequestParameters": { + "StateMachineArn": { + "Ref": "StateMachine2E01A3A5" + } + } + } + }, + "HttpApiPOSTasync8B2B0237": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "HttpApiF5A9A8A7" + }, + "RouteKey": "POST /async", + "AuthorizationType": "NONE", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "HttpApiPOSTasyncHttpIntegrationd384b5f9186315e0976ef6d3916dc4f68D037EA2" + } + ] + ] + } + } + }, + "HttpApiPOSTsyncHttpIntegration38c2878703d03e8c4eef45d83ec276209628FABA": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "HttpApiF5A9A8A7" + }, + "IntegrationType": "AWS_PROXY", + "CredentialsArn": { + "Fn::GetAtt": [ + "StateMachineRole78EC082E", + "Arn" + ] + }, + "IntegrationSubtype": "StepFunctions-StartSyncExecution", + "PayloadFormatVersion": "1.0", + "RequestParameters": { + "StateMachineArn": { + "Ref": "ExpressEE4D4F3B" + } + } + } + }, + "HttpApiPOSTsync9FBFB309": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "HttpApiF5A9A8A7" + }, + "RouteKey": "POST /sync", + "AuthorizationType": "NONE", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "HttpApiPOSTsyncHttpIntegration38c2878703d03e8c4eef45d83ec276209628FABA" + } + ] + ] + } + } + }, + "HttpApiDELETEHttpIntegrationb21c3be4c96769a184f67d698d49c9840806BE76": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "HttpApiF5A9A8A7" + }, + "IntegrationType": "AWS_PROXY", + "CredentialsArn": { + "Fn::GetAtt": [ + "StateMachineRole78EC082E", + "Arn" + ] + }, + "IntegrationSubtype": "StepFunctions-StopExecution", + "PayloadFormatVersion": "1.0", + "RequestParameters": { + "ExecutionArn": "$request.querystring.execution" + } + } + }, + "HttpApiDELETE73E582F0": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "HttpApiF5A9A8A7" + }, + "RouteKey": "DELETE /", + "AuthorizationType": "NONE", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "HttpApiDELETEHttpIntegrationb21c3be4c96769a184f67d698d49c9840806BE76" + } + ] + ] + } + } + } + }, + "Outputs": { + "HttpApiUrl": { + "Value": { + "Fn::GetAtt": [ + "HttpApiF5A9A8A7", + "ApiEndpoint" + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.step-functions.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.step-functions.ts new file mode 100644 index 0000000000000..365d243ff91e7 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.step-functions.ts @@ -0,0 +1,67 @@ +import { HttpApi, HttpMethod } from '@aws-cdk/aws-apigatewayv2'; +import { Role, ServicePrincipal } from '@aws-cdk/aws-iam'; +import { StateMachine, StateMachineType, Wait, WaitTime } from '@aws-cdk/aws-stepfunctions'; +import { App, CfnOutput, Duration, Stack } from '@aws-cdk/core'; +import { Mapping, StateMachineMappingExpression, StepFunctionsStartExecutionIntegration, StepFunctionsStartSyncExecutionIntegration, StepFunctionsStopExecutionIntegration, StringMappingExpression } from '../../lib'; + +// Stack verification steps +// deploy the stack and find the API Url from the stack outputs +// send a POST request to /sync and confirm that the standard step function +// is started. Note the execution ARN +// send a DELETE request to /?execution=; confirm that the function +// is stopped +// send a POST request to /sync; confirm that it completes after 5 seconds. + +const app = new App(); +const stack = new Stack(app, 'integ-step-functions'); + +const stateMachine = new StateMachine(stack, 'StateMachine', { + definition: new Wait(stack, 'Waiter', { + time: WaitTime.duration(Duration.seconds(30)), + }), +}); +const role = new Role(stack, 'StateMachineRole', { + assumedBy: new ServicePrincipal('apigateway.amazonaws.com'), +}); +stateMachine.grantStartExecution(role); +stateMachine.grantExecution(role, 'states:StopExecution'); + +const expressMachine = new StateMachine(stack, 'Express', { + definition: new Wait(stack, 'ExpressWait', { + time: WaitTime.duration(Duration.seconds(5)), + }), + stateMachineType: StateMachineType.EXPRESS, +}); +expressMachine.grant(role, 'states:StartSyncExecution'); + +const httpApi = new HttpApi(stack, 'HttpApi'); +httpApi.addRoutes({ + path: '/async', + methods: [HttpMethod.POST], + integration: new StepFunctionsStartExecutionIntegration({ + role, + stateMachine: StateMachineMappingExpression.fromStateMachine(stateMachine), + }), +}); + +httpApi.addRoutes({ + path: '/sync', + methods: [HttpMethod.POST], + integration: new StepFunctionsStartSyncExecutionIntegration({ + role, + stateMachine: StateMachineMappingExpression.fromStateMachine(expressMachine), + }), +}); + +httpApi.addRoutes({ + path: '/', + methods: [HttpMethod.DELETE], + integration: new StepFunctionsStopExecutionIntegration({ + role, + executionArn: StringMappingExpression.fromMapping(Mapping.fromQueryParam('execution')), + }), +}); + +new CfnOutput(stack, 'HttpApiUrl', { + value: httpApi.apiEndpoint, +}); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/kinesis.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/kinesis.test.ts new file mode 100644 index 0000000000000..d49a41729049e --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/kinesis.test.ts @@ -0,0 +1,76 @@ +import '@aws-cdk/assert-internal/jest'; +import { HttpApi, HttpRoute, HttpRouteKey } from '@aws-cdk/aws-apigatewayv2'; +import { Role } from '@aws-cdk/aws-iam'; +import { Stream } from '@aws-cdk/aws-kinesis'; +import { Stack } from '@aws-cdk/core'; +import { Mapping, StreamMappingExpression, StringMappingExpression } from '../../lib'; +import { KinesisPutRecordIntegration } from '../../lib/http/aws-proxy'; + +describe('Kinesis Integration', () => { + describe('PutRecord', () => { + test('minimum integration', () => { + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'API'); + const stream = Stream.fromStreamArn(stack, 'Stream', 'arn:aws:kinesis:::stream/Minimum'); + const streamMapping = StreamMappingExpression.fromStream(stream); + const role = Role.fromRoleArn(stack, 'Role', 'arn:aws:iam::123456789012:role/put'); + new HttpRoute(stack, 'Route', { + httpApi, + routeKey: HttpRouteKey.DEFAULT, + integration: new KinesisPutRecordIntegration({ + stream: streamMapping, + partitionKey: StringMappingExpression.fromValue('key'), + data: StringMappingExpression.fromMapping(Mapping.fromRequestBody()), + role, + }), + }); + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'AWS_PROXY', + IntegrationSubtype: 'Kinesis-PutRecord', + PayloadFormatVersion: '1.0', + CredentialsArn: 'arn:aws:iam::123456789012:role/put', + RequestParameters: { + StreamName: stream.streamName, + Data: '$request.body', + PartitionKey: 'key', + }, + }); + }); + test('full integration', () => { + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'API'); + const role = Role.fromRoleArn(stack, 'Role', 'arn:aws:iam::123456789012:role/kinesis-put'); + const stream = StreamMappingExpression.fromStream( + Stream.fromStreamArn(stack, 'Stream', 'arn:aws:kinesis:::stream/Integration'), + ); + new HttpRoute(stack, 'Route', { + httpApi, + routeKey: HttpRouteKey.DEFAULT, + integration: new KinesisPutRecordIntegration({ + stream, + data: StringMappingExpression.fromMapping(Mapping.fromRequestBody()), + partitionKey: StringMappingExpression.fromValue('key'), + sequenceNumberForOrdering: StringMappingExpression.fromValue('sequence'), + explicitHashKey: StringMappingExpression.fromValue('hashKey'), + role, + region: 'eu-north-1', + }), + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'AWS_PROXY', + IntegrationSubtype: 'Kinesis-PutRecord', + PayloadFormatVersion: '1.0', + CredentialsArn: 'arn:aws:iam::123456789012:role/kinesis-put', + RequestParameters: { + StreamName: 'Integration', + Data: '$request.body', + PartitionKey: 'key', + SequenceNumberForOrdering: 'sequence', + ExplicitHashKey: 'hashKey', + Region: 'eu-north-1', + }, + }); + }); + }); +}); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/sqs.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/sqs.test.ts new file mode 100644 index 0000000000000..826a756e46f36 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/sqs.test.ts @@ -0,0 +1,288 @@ +import '@aws-cdk/assert-internal/jest'; +import { HttpApi, HttpRoute, HttpRouteKey } from '@aws-cdk/aws-apigatewayv2'; +import { IRole, Role } from '@aws-cdk/aws-iam'; +import { Queue } from '@aws-cdk/aws-sqs'; +import { Duration, Stack } from '@aws-cdk/core'; +import { + SqsMessageAttributeListMappingExpression, + SqsMessageAttribute, + SqsDeleteMessageIntegration, + SqsPurgeQueueIntegration, + SqsReceiveMessageIntegration, + SqsSendMessageIntegration, +} from '../../lib/http/aws-proxy'; +import { ArrayMappingExpression, DurationMappingExpression, Mapping, QueueMappingExpression, SqsAttributeListMappingExpression, SqsStringAttribute, StringMappingExpression } from '../../lib/http/mapping-expression'; + +describe('SQS Integrations', () => { + describe('SendMessage', () => { + test('basic integration', () => { + const { stack, api, queue, role } = setupTestFixtures( + 'arn:aws:sqs:eu-west-2:123456789012:queue', + 'arn:aws:iam::123456789012:role/send', + ); + new HttpRoute(stack, 'Route', { + httpApi: api, + integration: new SqsSendMessageIntegration({ + queue, + body: StringMappingExpression.fromValue('message'), + role, + }), + routeKey: HttpRouteKey.with('/sendMessage'), + }); + + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'AWS_PROXY', + IntegrationSubtype: 'SQS-SendMessage', + PayloadFormatVersion: '1.0', + CredentialsArn: 'arn:aws:iam::123456789012:role/send', + RequestParameters: { + QueueUrl: makeQueueUrl('eu-west-2', '123456789012', 'queue'), + MessageBody: 'message', + }, + }); + }); + test('full integration', () => { + const { stack, api, queue, role } = setupTestFixtures( + 'arn:aws:sqs:us-east-1:123456789012:queue', + 'arn:aws:iam::123456789012:role/sqs-role', + ); + new HttpRoute(stack, 'Full', { + httpApi: api, + integration: new SqsSendMessageIntegration({ + body: StringMappingExpression.fromValue('message-body'), + queue, + role, + attributes: SqsAttributeListMappingExpression.fromSqsAttributeList([new SqsStringAttribute('some-attributes', 'value')]), + deduplicationId: StringMappingExpression.fromValue('$request.id'), + delay: DurationMappingExpression.fromDuration(Duration.seconds(4)), + groupId: StringMappingExpression.fromValue('the-group'), + region: 'us-east-1', + systemAttributes: StringMappingExpression.fromValue('system-attrs'), + }), + routeKey: HttpRouteKey.DEFAULT, + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'AWS_PROXY', + IntegrationSubtype: 'SQS-SendMessage', + PayloadFormatVersion: '1.0', + CredentialsArn: 'arn:aws:iam::123456789012:role/sqs-role', + RequestParameters: { + QueueUrl: makeQueueUrl('us-east-1', '123456789012', 'queue'), + DelaySeconds: '4', + MessageAttributes: '{"some-attributes":{"DataType":"String","StringValue":"value"}}', + MessageBody: 'message-body', + MessageDeduplicationId: '$request.id', + MessageGroupId: 'the-group', + Region: 'us-east-1', + MessageSystemAttributes: 'system-attrs', + }, + }); + }); + }); + + describe('ReceiveMessage', () => { + test('minimum integration', () => { + const { stack, api, queue, role } = setupTestFixtures( + 'arn:aws:sqs:us-east-1:123456789012:receive-queue', + 'arn:aws:iam::123456789012:role/receive', + ); + new HttpRoute(stack, 'Route', { + httpApi: api, + routeKey: HttpRouteKey.with('/messages'), + integration: new SqsReceiveMessageIntegration({ + queue, + role, + }), + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'AWS_PROXY', + IntegrationSubtype: 'SQS-ReceiveMessage', + PayloadFormatVersion: '1.0', + CredentialsArn: 'arn:aws:iam::123456789012:role/receive', + RequestParameters: { + QueueUrl: makeQueueUrl('us-east-1', '123456789012', 'receive-queue'), + }, + }); + }); + test('full integration', () => { + const { stack, api, role, queue } = setupTestFixtures( + 'arn:aws:sqs:us-west-1:123456789012:receive-queue.fifo', + 'arn:aws:iam::123456789012:role/sqs-receive', + ); + + new HttpRoute(stack, 'Route', { + httpApi: api, + integration: new SqsReceiveMessageIntegration({ + role, + queue, + attributeNames: SqsMessageAttributeListMappingExpression.fromAttributeList([SqsMessageAttribute.ALL]), + maxNumberOfMessages: StringMappingExpression.fromValue('2'), + messageAttributeNames: ArrayMappingExpression.fromValue(['Attribute1']), + receiveRequestAttemptId: StringMappingExpression.fromValue('rra-id'), + visibilityTimeout: DurationMappingExpression.fromDuration(Duration.seconds(30)), + waitTime: DurationMappingExpression.fromDuration(Duration.seconds(4)), + region: 'eu-central-1', + }), + routeKey: HttpRouteKey.DEFAULT, + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'AWS_PROXY', + IntegrationSubtype: 'SQS-ReceiveMessage', + PayloadFormatVersion: '1.0', + CredentialsArn: 'arn:aws:iam::123456789012:role/sqs-receive', + RequestParameters: { + QueueUrl: makeQueueUrl('us-west-1', '123456789012', 'receive-queue.fifo'), + AttributeNames: '["All"]', + MaxNumberOfMessages: '2', + MessageAttributeNames: '["Attribute1"]', + ReceiveRequestAttemptId: 'rra-id', + VisibilityTimeout: '30', + WaitTimeSeconds: '4', + Region: 'eu-central-1', + }, + }); + }); + }); + describe('DeleteMessage', () => { + test('minimum integration', () => { + const { stack, api, queue, role } = setupTestFixtures( + 'arn:aws:sqs:eu-central-1:123456789012:delete', + 'arn:aws:iam::123456789012:role/delete', + ); + new HttpRoute(stack, 'Route', { + httpApi: api, + routeKey: HttpRouteKey.with('/delete'), + integration: new SqsDeleteMessageIntegration({ + queue, + receiptHandle: StringMappingExpression.fromMapping(Mapping.fromRequestBody()), + role, + }), + }); + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'AWS_PROXY', + IntegrationSubtype: 'SQS-DeleteMessage', + PayloadFormatVersion: '1.0', + CredentialsArn: 'arn:aws:iam::123456789012:role/delete', + RequestParameters: { + QueueUrl: makeQueueUrl('eu-central-1', '123456789012', 'delete'), + ReceiptHandle: '$request.body', + }, + }); + }); + test('full integration', () => { + const { stack, api, role, queue } = setupTestFixtures( + 'arn:aws:sqs:eu-west-2:123456789012:queue', + 'arn:aws:iam::123456789012:role/sqs-delete', + ); + new HttpRoute(stack, 'Route', { + httpApi: api, + integration: new SqsDeleteMessageIntegration({ + queue, + receiptHandle: StringMappingExpression.fromValue('MbZj6wDWli%2BJvwwJaBV%2B3dcjk2YW2vA3%2BSTFFljT'), + role, + region: 'eu-west-1', + }), + routeKey: HttpRouteKey.DEFAULT, + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'AWS_PROXY', + IntegrationSubtype: 'SQS-DeleteMessage', + PayloadFormatVersion: '1.0', + CredentialsArn: 'arn:aws:iam::123456789012:role/sqs-delete', + RequestParameters: { + QueueUrl: makeQueueUrl('eu-west-2', '123456789012', 'queue'), + ReceiptHandle: 'MbZj6wDWli%2BJvwwJaBV%2B3dcjk2YW2vA3%2BSTFFljT', + Region: 'eu-west-1', + }, + }); + }); + }); + describe('PurgeQueue', () => { + test('minimum integration', () => { + const { stack, api, queue, role } = setupTestFixtures( + 'arn:aws:sqs:eu-west-1:123456789012:queue', + 'arn:aws:iam::123456789012:role/purge', + ); + new HttpRoute(stack, 'Route', { + httpApi: api, + routeKey: HttpRouteKey.with('/purge'), + integration: new SqsPurgeQueueIntegration({ + queue, + role, + }), + }); + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'AWS_PROXY', + IntegrationSubtype: 'SQS-PurgeQueue', + PayloadFormatVersion: '1.0', + CredentialsArn: 'arn:aws:iam::123456789012:role/purge', + RequestParameters: { + QueueUrl: makeQueueUrl('eu-west-1', '123456789012', 'queue'), + }, + }); + }); + test('full integration', () => { + const { stack, api, queue, role } = setupTestFixtures( + 'arn:aws:sqs:eu-west-1:123456789012:queue', + 'arn:aws:iam::123456789012:role/purge', + ); + new HttpRoute(stack, 'Route', { + httpApi: api, + integration: new SqsPurgeQueueIntegration({ + queue, + role, + region: 'us-east-2', + }), + routeKey: HttpRouteKey.DEFAULT, + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'AWS_PROXY', + IntegrationSubtype: 'SQS-PurgeQueue', + PayloadFormatVersion: '1.0', + CredentialsArn: 'arn:aws:iam::123456789012:role/purge', + RequestParameters: { + QueueUrl: makeQueueUrl('eu-west-1', '123456789012', 'queue'), + Region: 'us-east-2', + }, + }); + }); + }); +}); + +function setupTestFixtures(): { stack: Stack, api: HttpApi }; +function setupTestFixtures(queueArn: string, roleArn: string): { + stack: Stack, + api: HttpApi, + queue: QueueMappingExpression, + role: IRole, +}; +function setupTestFixtures(queueArn?: string, roleArn?: string) { + const stack = new Stack(); + return { + stack, + api: new HttpApi(stack, 'API'), + queue: queueArn && QueueMappingExpression.fromQueue(Queue.fromQueueArn(stack, 'Queue', queueArn)), + role: roleArn && Role.fromRoleArn(stack, 'Role', roleArn), + }; +} + +function makeQueueUrl(region: string, account: string, queueName: string) { + return { + 'Fn::Join': [ + '', + [ + `https://sqs.${region}.`, + { + Ref: 'AWS::URLSuffix', + }, + `/${account}/${queueName}`, + ], + ], + }; +} diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/step-functions.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/step-functions.test.ts new file mode 100644 index 0000000000000..aa5ef6e1a0af3 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/step-functions.test.ts @@ -0,0 +1,211 @@ +import '@aws-cdk/assert-internal/jest'; +import { HttpApi, HttpRoute, HttpRouteKey } from '@aws-cdk/aws-apigatewayv2'; +import { IRole, Role } from '@aws-cdk/aws-iam'; +import { StateMachine } from '@aws-cdk/aws-stepfunctions'; +import { Stack } from '@aws-cdk/core'; +import { Mapping, StateMachineMappingExpression, StringMappingExpression } from '../../lib'; +import { StepFunctionsStartExecutionIntegration, StepFunctionsStartSyncExecutionIntegration, StepFunctionsStopExecutionIntegration } from '../../lib/http/aws-proxy'; + +describe('Step Functions integrations', () => { + describe('StartExecution', () => { + test('minimum integration', () => { + const { stack, httpApi, role } = createTestFixtures('arn:aws:iam::123456789012:role/start'); + const stateMachine = StateMachine.fromStateMachineArn( + stack, + 'StateMachine', + 'arn:aws:states:eu-central-2:123456789012:stateMachine:minimum', + ); + const stateMachineMapping = StateMachineMappingExpression.fromStateMachine(stateMachine); + new HttpRoute(stack, 'Route', { + httpApi, + routeKey: HttpRouteKey.DEFAULT, + integration: new StepFunctionsStartExecutionIntegration({ + stateMachine: stateMachineMapping, + role, + }), + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'AWS_PROXY', + IntegrationSubtype: 'StepFunctions-StartExecution', + PayloadFormatVersion: '1.0', + CredentialsArn: 'arn:aws:iam::123456789012:role/start', + RequestParameters: { + StateMachineArn: stateMachine.stateMachineArn, + }, + }); + }); + test('full integration', () => { + const { stack, httpApi, role } = createTestFixtures('arn:aws:iam::123456789012:role/start'); + const stateMachine = StateMachine.fromStateMachineArn( + stack, + 'StateMachine', + 'arn:aws:states:us-west-1:123456789012:stateMachine:my-state-machine', + ); + const stateMachineMapping = StateMachineMappingExpression.fromStateMachine(stateMachine); + new HttpRoute(stack, 'Route', { + httpApi, + routeKey: HttpRouteKey.DEFAULT, + integration: new StepFunctionsStartExecutionIntegration({ + role, + stateMachine: stateMachineMapping, + name: StringMappingExpression.fromValue('execution-name'), + input: StringMappingExpression.fromMapping(Mapping.fromRequestBody()), + region: 'us-west-2', + }), + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'AWS_PROXY', + IntegrationSubtype: 'StepFunctions-StartExecution', + PayloadFormatVersion: '1.0', + CredentialsArn: 'arn:aws:iam::123456789012:role/start', + RequestParameters: { + StateMachineArn: 'arn:aws:states:us-west-1:123456789012:stateMachine:my-state-machine', + Name: 'execution-name', + Input: '$request.body', + Region: 'us-west-2', + }, + }); + }); + }); + + describe('StartSyncExecution', () => { + test('minimum integration', () => { + const { stack, httpApi, role } = createTestFixtures( + 'arn:aws:iam::123456789012:role/start-sync', + ); + const stateMachine = StateMachine.fromStateMachineArn( + stack, + 'StateMachine', + 'arn:aws:states:eu-central-2:123456789012:stateMachine:minimum', + ); + const stateMachineMapping = StateMachineMappingExpression.fromStateMachine(stateMachine); + new HttpRoute(stack, 'Route', { + httpApi, + routeKey: HttpRouteKey.DEFAULT, + integration: new StepFunctionsStartSyncExecutionIntegration({ + stateMachine: stateMachineMapping, + role, + }), + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'AWS_PROXY', + IntegrationSubtype: 'StepFunctions-StartSyncExecution', + PayloadFormatVersion: '1.0', + CredentialsArn: 'arn:aws:iam::123456789012:role/start-sync', + RequestParameters: { + StateMachineArn: stateMachine.stateMachineArn, + }, + }); + }); + test('full integration', () => { + const { stack, httpApi, role } = createTestFixtures( + 'arn:aws:iam::123456789012:role/start-sync', + ); + const stateMachine = StateMachine.fromStateMachineArn( + stack, + 'StateMachine', + 'arn:aws:states:us-west-2:123456789012:stateMachine:sync', + ); + const stateMachineMapping = StateMachineMappingExpression.fromStateMachine(stateMachine); + new HttpRoute(stack, 'Route', { + httpApi, + routeKey: HttpRouteKey.DEFAULT, + integration: new StepFunctionsStartSyncExecutionIntegration({ + role, + stateMachine: stateMachineMapping, + name: StringMappingExpression.fromValue('execution'), + input: StringMappingExpression.fromValue('{}'), + traceHeader: StringMappingExpression.fromMapping(Mapping.fromRequestHeader('X-My-Trace-ID')), + region: 'us-east-2', + }), + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'AWS_PROXY', + IntegrationSubtype: 'StepFunctions-StartSyncExecution', + PayloadFormatVersion: '1.0', + CredentialsArn: 'arn:aws:iam::123456789012:role/start-sync', + RequestParameters: { + StateMachineArn: 'arn:aws:states:us-west-2:123456789012:stateMachine:sync', + Name: 'execution', + Input: '{}', + TraceHeader: '$request.header.X-My-Trace-ID', + Region: 'us-east-2', + }, + }); + }); + }); + + describe('StopExecution', () => { + test('minimum integration', () => { + const { stack, httpApi, role } = createTestFixtures('arn:aws:iam::123456789012:role/stop'); + new HttpRoute(stack, 'Route', { + httpApi, + routeKey: HttpRouteKey.DEFAULT, + integration: new StepFunctionsStopExecutionIntegration({ + executionArn: StringMappingExpression.fromValue( + 'arn:aws:states:us-east-2:123456789012:executions:state:my-execution', + ), + role, + }), + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'AWS_PROXY', + IntegrationSubtype: 'StepFunctions-StopExecution', + PayloadFormatVersion: '1.0', + CredentialsArn: 'arn:aws:iam::123456789012:role/stop', + RequestParameters: { + ExecutionArn: 'arn:aws:states:us-east-2:123456789012:executions:state:my-execution', + }, + }); + }); + test('full integration', () => { + const { stack, httpApi, role } = createTestFixtures( + 'arn:aws:iam::123456789012:role/stop', + ); + new HttpRoute(stack, 'Route', { + httpApi, + routeKey: HttpRouteKey.DEFAULT, + integration: new StepFunctionsStopExecutionIntegration({ + executionArn: StringMappingExpression.fromValue( + 'arn:aws:states:us-east-2:123456789012:executions:state:my-execution', + ), + error: StringMappingExpression.fromMapping(Mapping.fromRequestBody('error')), + cause: StringMappingExpression.fromMapping(Mapping.fromRequestBody('cause')), + role, + region: 'eu-west-2', + }), + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'AWS_PROXY', + IntegrationSubtype: 'StepFunctions-StopExecution', + PayloadFormatVersion: '1.0', + CredentialsArn: 'arn:aws:iam::123456789012:role/stop', + RequestParameters: { + ExecutionArn: 'arn:aws:states:us-east-2:123456789012:executions:state:my-execution', + Error: '$request.body.error', + Cause: '$request.body.cause', + Region: 'eu-west-2', + }, + }); + }); + }); +}); + +function createTestFixtures(): { stack: Stack, httpApi: HttpApi }; +function createTestFixtures(roleArn: string): { stack: Stack, httpApi: HttpApi, role: IRole }; +function createTestFixtures(roleArn?: string) { + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'API'); + const role = roleArn && Role.fromRoleArn(stack, 'Role', roleArn); + return { + stack, + httpApi, + role, + }; +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts index 094c0131872d8..f8641de969881 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts @@ -148,7 +148,7 @@ export interface HttpApiProps { /** * Supported CORS HTTP methods */ -export enum CorsHttpMethod{ +export enum CorsHttpMethod { /** HTTP ANY */ ANY = '*', /** HTTP DELETE */ @@ -299,12 +299,15 @@ abstract class HttpApiBase extends ApiBase implements IHttpApi { // note that th const integration = new HttpIntegration(scope, `HttpIntegration-${configHash}`, { httpApi: this, integrationType: config.type, + integrationSubtype: config.subtype, integrationUri: config.uri, + credentials: config.credentials, method: config.method, connectionId: config.connectionId, connectionType: config.connectionType, payloadFormatVersion: config.payloadFormatVersion, secureServerName: config.secureServerName, + requestParameters: config.requestParameters, }); this._integrationCache.saveIntegration(scope, config, integration); diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts index f832b5b7e3b21..353f76d9f3031 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts @@ -1,4 +1,5 @@ /* eslint-disable quotes */ +import { IRole } from '@aws-cdk/aws-iam'; import { Resource } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnIntegration } from '../apigatewayv2.generated'; @@ -74,6 +75,27 @@ export class PayloadFormatVersion { } } +/** + * Credentials used for AWS Service integrations. + */ +export class IntegrationCredentials { + /** Use the specified role for integration requests */ + public static fromRole(role: IRole): IntegrationCredentials { + return new IntegrationCredentials(role.roleArn); + } + /** Use the calling user's identity to call the integration */ + public static useCallerIdentity(): IntegrationCredentials { + return new IntegrationCredentials('arn:aws:iam::*:user/*'); + } + + private constructor( + /** + * The credential ARN for the integration + */ + readonly credentials: string, + ) { } +} + /** * The integration properties */ @@ -88,12 +110,21 @@ export interface HttpIntegrationProps { */ readonly integrationType: HttpIntegrationType; + /** + * Integration subtype. + * Used for AWS Service integrations, specifies the target of the integration. + * @default - none. required if no integrationUri is defined. + */ + readonly integrationSubtype?: HttpIntegrationSubtype; + /** * Integration URI. * This will be the function ARN in the case of `HttpIntegrationType.LAMBDA_PROXY`, - * or HTTP URL in the case of `HttpIntegrationType.HTTP_PROXY`. + * or HTTP URL in the case of `HttpIntegrationType.HTTP_PROXY`. Not set for AWS Service + * integrations. + * @default - none. required if no integrationSubtype is defined. */ - readonly integrationUri: string; + readonly integrationUri?: string; /** * The HTTP method to use when calling the underlying HTTP proxy @@ -115,6 +146,12 @@ export interface HttpIntegrationProps { */ readonly connectionType?: HttpConnectionType; + /** + * The credentials with which to invoke the integration. + * @default - undefined + */ + readonly credentials?: IntegrationCredentials; + /** * The version of the payload format * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html @@ -128,6 +165,18 @@ export interface HttpIntegrationProps { * @default undefined private integration traffic will use HTTP protocol */ readonly secureServerName?: string; + + /** + * Mappings to apply to the request before it is sent to the integration. + * For AWS Service integrations, these define how the request is mapped onto the service. + * For HTTP integrations, these can transform aspects of the request for the target. + * + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-aws-services.html + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-parameter-mapping.html + * + * @default - undefined + */ + readonly requestParameters?: object; } /** @@ -144,11 +193,14 @@ export class HttpIntegration extends Resource implements IHttpIntegration { const integ = new CfnIntegration(this, 'Resource', { apiId: props.httpApi.apiId, integrationType: props.integrationType, + integrationSubtype: props.integrationSubtype, integrationUri: props.integrationUri, integrationMethod: props.method, connectionId: props.connectionId, connectionType: props.connectionType, + credentialsArn: props.credentials?.credentials, payloadFormatVersion: props.payloadFormatVersion?.version, + requestParameters: props.requestParameters, }); if (props.secureServerName) { @@ -162,6 +214,53 @@ export class HttpIntegration extends Resource implements IHttpIntegration { } } +/** + * Supported integration subtypes + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-aws-services-reference.html + */ +export enum HttpIntegrationSubtype { + /** + * EventBridge PutEvents integration + */ + EVENTBRIDGE_PUTEVENTS = 'EventBridge-PutEvents', + /** + * SQS SendMessage integration + */ + SQS_SENDMESSAGE = 'SQS-SendMessage', + /** + * SQS ReceiveMessage integration, + */ + SQS_RECEIVEMESSAGE = 'SQS-ReceiveMessage', + /** + * SQS DeleteMessage integration, + */ + SQS_DELETEMESSAGE = 'SQS-DeleteMessage', + /** + * SQS PurgeQueue integration + */ + SQS_PURGEQUEUE = 'SQS-PurgeQueue', + /** + * AppConfig GetConfiguration integration + */ + APPCONFIG_GETCONFIGURATION = 'AppConfig-GetConfiguration', + /** + * Kinesis PutRecord integration + */ + KINESIS_PUTRECORD = 'Kinesis-PutRecord', + /** + * Step Functions StartExecution integration + */ + STEPFUNCTIONS_STARTEXECUTION = 'StepFunctions-StartExecution', + /** + * Step Functions StartSyncExecution integration + */ + STEPFUNCTIONS_STARTSYNCEXECUTION = 'StepFunctions-StartSyncExecution', + /** + * Step Functions StopExecution integration + */ + STEPFUNCTIONS_STOPEXECUTION = 'StepFunctions-StopExecution', +} + /** * Options to the HttpRouteIntegration during its bind operation. */ @@ -199,9 +298,17 @@ export interface HttpRouteIntegrationConfig { readonly type: HttpIntegrationType; /** - * Integration URI + * Integration subtype. + * Used for AWS Service integrations only. + * @default - none. required if no uri specified. */ - readonly uri: string; + readonly subtype?: HttpIntegrationSubtype; + + /** + * Integration URI. + * @default - none. required if no subtype specified. + */ + readonly uri?: string; /** * The HTTP method that must be used to invoke the underlying proxy. @@ -224,6 +331,12 @@ export interface HttpRouteIntegrationConfig { */ readonly connectionType?: HttpConnectionType; + /** + * The identity to use for the integration. Only applicable to AWS Service integrations. + * @default - undefined + */ + readonly credentials?: IntegrationCredentials; + /** * Payload format version in the case of lambda proxy integration * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html @@ -237,4 +350,12 @@ export interface HttpRouteIntegrationConfig { * @default undefined private integration traffic will use HTTP protocol */ readonly secureServerName?: string; + + /** + * Mappings to apply to the request before it is sent to the integration. + * For AWS Service integrations, these define how the request is mapped onto the service. + * For HTTP integrations, these can transform aspects of the request for the target. + * @default - undefined + */ + readonly requestParameters?: object; }