From 4d7374ea5b83c4341935f5e5b39429b662c3857d Mon Sep 17 00:00:00 2001 From: GZ Date: Thu, 18 Jan 2024 10:04:58 -0800 Subject: [PATCH] feat(apigatewayv2): AWS type websocket api integration in http api (#28718) Currently, Amazon.CDK.AWS.Apigatewayv2 lacks support for AWS option as the [IntegrationType](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigatewayv2-integration.html#cfn-apigatewayv2-integration-integrationtype) for WebSocket Apigateway. Added the capability that allows user to create a WebSocket Apigateway that calls directly other AWS services without a Lambda function middleware. Closes https://github.com/aws/aws-cdk/issues/27164. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- ...efaultTestDeployAssert2B785EAB.assets.json | 19 + ...aultTestDeployAssert2B785EAB.template.json | 36 ++ .../websocket/integ.aws.js.snapshot/cdk.out | 1 + ...nteg-aws-websocket-integration.assets.json | 19 + ...eg-aws-websocket-integration.template.json | 202 +++++++++ .../integ.aws.js.snapshot/integ.json | 19 + .../integ.aws.js.snapshot/manifest.json | 181 ++++++++ .../websocket/integ.aws.js.snapshot/tree.json | 423 ++++++++++++++++++ .../test/websocket/integ.aws.ts | 67 +++ .../aws-apigatewayv2-integrations/README.md | 42 ++ .../lib/websocket/aws.ts | 80 ++++ .../lib/websocket/index.ts | 1 + .../test/websocket/aws.test.ts | 28 ++ .../lib/websocket/integration.ts | 94 +++- 14 files changed, 1211 insertions(+), 1 deletion(-) create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/apigatewayv2awsintegrationintegtestDefaultTestDeployAssert2B785EAB.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/apigatewayv2awsintegrationintegtestDefaultTestDeployAssert2B785EAB.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/cdk.out create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/integ-aws-websocket-integration.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/integ-aws-websocket-integration.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/integ.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/manifest.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/tree.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.ts create mode 100644 packages/aws-cdk-lib/aws-apigatewayv2-integrations/lib/websocket/aws.ts create mode 100644 packages/aws-cdk-lib/aws-apigatewayv2-integrations/test/websocket/aws.test.ts diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/apigatewayv2awsintegrationintegtestDefaultTestDeployAssert2B785EAB.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/apigatewayv2awsintegrationintegtestDefaultTestDeployAssert2B785EAB.assets.json new file mode 100644 index 0000000000000..a2db452b31120 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/apigatewayv2awsintegrationintegtestDefaultTestDeployAssert2B785EAB.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "apigatewayv2awsintegrationintegtestDefaultTestDeployAssert2B785EAB.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/apigatewayv2awsintegrationintegtestDefaultTestDeployAssert2B785EAB.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/apigatewayv2awsintegrationintegtestDefaultTestDeployAssert2B785EAB.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/apigatewayv2awsintegrationintegtestDefaultTestDeployAssert2B785EAB.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/cdk.out new file mode 100644 index 0000000000000..1f0068d32659a --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"36.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/integ-aws-websocket-integration.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/integ-aws-websocket-integration.assets.json new file mode 100644 index 0000000000000..93e8c3b698172 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/integ-aws-websocket-integration.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.0", + "files": { + "165e169601b6d918afe762f370d9ce4ceb525ddb21c3aa95abf339d5fd6b532c": { + "source": { + "path": "integ-aws-websocket-integration.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "165e169601b6d918afe762f370d9ce4ceb525ddb21c3aa95abf339d5fd6b532c.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/integ-aws-websocket-integration.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/integ-aws-websocket-integration.template.json new file mode 100644 index 0000000000000..af961456f5acb --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/integ-aws-websocket-integration.template.json @@ -0,0 +1,202 @@ +{ + "Resources": { + "MyTable794EDED1": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "TableName": "MyTable" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "ApiGatewayRoleD2518903": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonDynamoDBFullAccess" + ] + ] + } + ] + } + }, + "mywsapi32E6CE11": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "mywsapi", + "ProtocolType": "WEBSOCKET", + "RouteSelectionExpression": "$request.body.action" + } + }, + "mywsapidefaultRouteDefaultIntegrationFFCB3BA9": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "IntegrationType": "MOCK", + "IntegrationUri": "" + } + }, + "mywsapidefaultRouteE9382DF8": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "AuthorizationType": "NONE", + "RouteKey": "$default", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mywsapidefaultRouteDefaultIntegrationFFCB3BA9" + } + ] + ] + } + } + }, + "mywsapiconnectRouteDynamodbPutItem9E189A39": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "CredentialsArn": { + "Fn::GetAtt": [ + "ApiGatewayRoleD2518903", + "Arn" + ] + }, + "IntegrationMethod": "POST", + "IntegrationType": "AWS", + "IntegrationUri": { + "Fn::Join": [ + "", + [ + "arn:aws:apigateway:", + { + "Ref": "AWS::Region" + }, + ":dynamodb:action/PutItem" + ] + ] + }, + "RequestTemplates": { + "application/json": { + "Fn::Join": [ + "", + [ + "{\"TableName\":\"", + { + "Ref": "MyTable794EDED1" + }, + "\",\"Item\":{\"id\":{\"S\":\"$context.requestId\"}}}" + ] + ] + } + } + } + }, + "mywsapiconnectRoute45A0ED6A": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "AuthorizationType": "NONE", + "RouteKey": "$connect", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mywsapiconnectRouteDynamodbPutItem9E189A39" + } + ] + ] + } + } + }, + "DevStage520A913F": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "AutoDeploy": true, + "StageName": "dev" + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/integ.json new file mode 100644 index 0000000000000..aa4f146b4e1ff --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/integ.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.0", + "testCases": { + "apigatewayv2-aws-integration-integ-test/DefaultTest": { + "stacks": [ + "integ-aws-websocket-integration" + ], + "cdkCommandOptions": { + "deploy": { + "args": { + "rollback": true + } + } + }, + "assertionStack": "apigatewayv2-aws-integration-integ-test/DefaultTest/DeployAssert", + "assertionStackName": "apigatewayv2awsintegrationintegtestDefaultTestDeployAssert2B785EAB" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/manifest.json new file mode 100644 index 0000000000000..24accf82b2dcf --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/manifest.json @@ -0,0 +1,181 @@ +{ + "version": "36.0.0", + "artifacts": { + "integ-aws-websocket-integration.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "integ-aws-websocket-integration.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "integ-aws-websocket-integration": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "integ-aws-websocket-integration.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/165e169601b6d918afe762f370d9ce4ceb525ddb21c3aa95abf339d5fd6b532c.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "integ-aws-websocket-integration.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "integ-aws-websocket-integration.assets" + ], + "metadata": { + "/integ-aws-websocket-integration/MyTable": [ + { + "type": "aws:cdk:hasPhysicalName", + "data": { + "Ref": "MyTable794EDED1" + } + } + ], + "/integ-aws-websocket-integration/MyTable/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyTable794EDED1" + } + ], + "/integ-aws-websocket-integration/ApiGatewayRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "ApiGatewayRoleD2518903" + } + ], + "/integ-aws-websocket-integration/mywsapi/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "mywsapi32E6CE11" + } + ], + "/integ-aws-websocket-integration/mywsapi/$default-Route/DefaultIntegration/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "mywsapidefaultRouteDefaultIntegrationFFCB3BA9" + } + ], + "/integ-aws-websocket-integration/mywsapi/$default-Route/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "mywsapidefaultRouteE9382DF8" + } + ], + "/integ-aws-websocket-integration/mywsapi/$connect-Route/DynamodbPutItem/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "mywsapiconnectRouteDynamodbPutItem9E189A39" + } + ], + "/integ-aws-websocket-integration/mywsapi/$connect-Route/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "mywsapiconnectRoute45A0ED6A" + } + ], + "/integ-aws-websocket-integration/DevStage/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "DevStage520A913F" + } + ], + "/integ-aws-websocket-integration/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/integ-aws-websocket-integration/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ], + "mywsapiitemRouteDynamodbPutItem27DCDF48": [ + { + "type": "aws:cdk:logicalId", + "data": "mywsapiitemRouteDynamodbPutItem27DCDF48", + "trace": [ + "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" + ] + } + ], + "mywsapiitemRoute86C9FFC9": [ + { + "type": "aws:cdk:logicalId", + "data": "mywsapiitemRoute86C9FFC9", + "trace": [ + "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" + ] + } + ] + }, + "displayName": "integ-aws-websocket-integration" + }, + "apigatewayv2awsintegrationintegtestDefaultTestDeployAssert2B785EAB.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "apigatewayv2awsintegrationintegtestDefaultTestDeployAssert2B785EAB.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "apigatewayv2awsintegrationintegtestDefaultTestDeployAssert2B785EAB": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "apigatewayv2awsintegrationintegtestDefaultTestDeployAssert2B785EAB.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "apigatewayv2awsintegrationintegtestDefaultTestDeployAssert2B785EAB.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "apigatewayv2awsintegrationintegtestDefaultTestDeployAssert2B785EAB.assets" + ], + "metadata": { + "/apigatewayv2-aws-integration-integ-test/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/apigatewayv2-aws-integration-integ-test/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "apigatewayv2-aws-integration-integ-test/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/tree.json new file mode 100644 index 0000000000000..e7e21f31223ef --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/tree.json @@ -0,0 +1,423 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "integ-aws-websocket-integration": { + "id": "integ-aws-websocket-integration", + "path": "integ-aws-websocket-integration", + "children": { + "MyTable": { + "id": "MyTable", + "path": "integ-aws-websocket-integration/MyTable", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-aws-websocket-integration/MyTable/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::DynamoDB::Table", + "aws:cdk:cloudformation:props": { + "attributeDefinitions": [ + { + "attributeName": "id", + "attributeType": "S" + } + ], + "billingMode": "PAY_PER_REQUEST", + "keySchema": [ + { + "attributeName": "id", + "keyType": "HASH" + } + ], + "tableName": "MyTable" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_dynamodb.CfnTable", + "version": "0.0.0" + } + }, + "ScalingRole": { + "id": "ScalingRole", + "path": "integ-aws-websocket-integration/MyTable/ScalingRole", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_dynamodb.Table", + "version": "0.0.0" + } + }, + "ApiGatewayRole": { + "id": "ApiGatewayRole", + "path": "integ-aws-websocket-integration/ApiGatewayRole", + "children": { + "ImportApiGatewayRole": { + "id": "ImportApiGatewayRole", + "path": "integ-aws-websocket-integration/ApiGatewayRole/ImportApiGatewayRole", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "integ-aws-websocket-integration/ApiGatewayRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "managedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonDynamoDBFullAccess" + ] + ] + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Role", + "version": "0.0.0" + } + }, + "mywsapi": { + "id": "mywsapi", + "path": "integ-aws-websocket-integration/mywsapi", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-aws-websocket-integration/mywsapi/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::Api", + "aws:cdk:cloudformation:props": { + "name": "mywsapi", + "protocolType": "WEBSOCKET", + "routeSelectionExpression": "$request.body.action" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_apigatewayv2.CfnApi", + "version": "0.0.0" + } + }, + "$default-Route": { + "id": "$default-Route", + "path": "integ-aws-websocket-integration/mywsapi/$default-Route", + "children": { + "DefaultIntegration": { + "id": "DefaultIntegration", + "path": "integ-aws-websocket-integration/mywsapi/$default-Route/DefaultIntegration", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-aws-websocket-integration/mywsapi/$default-Route/DefaultIntegration/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::Integration", + "aws:cdk:cloudformation:props": { + "apiId": { + "Ref": "mywsapi32E6CE11" + }, + "integrationType": "MOCK", + "integrationUri": "" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_apigatewayv2.CfnIntegration", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_apigatewayv2.WebSocketIntegration", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "integ-aws-websocket-integration/mywsapi/$default-Route/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::Route", + "aws:cdk:cloudformation:props": { + "apiId": { + "Ref": "mywsapi32E6CE11" + }, + "authorizationType": "NONE", + "routeKey": "$default", + "target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mywsapidefaultRouteDefaultIntegrationFFCB3BA9" + } + ] + ] + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_apigatewayv2.CfnRoute", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_apigatewayv2.WebSocketRoute", + "version": "0.0.0" + } + }, + "$connect-Route": { + "id": "$connect-Route", + "path": "integ-aws-websocket-integration/mywsapi/$connect-Route", + "children": { + "DynamodbPutItem": { + "id": "DynamodbPutItem", + "path": "integ-aws-websocket-integration/mywsapi/$connect-Route/DynamodbPutItem", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-aws-websocket-integration/mywsapi/$connect-Route/DynamodbPutItem/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::Integration", + "aws:cdk:cloudformation:props": { + "apiId": { + "Ref": "mywsapi32E6CE11" + }, + "credentialsArn": { + "Fn::GetAtt": [ + "ApiGatewayRoleD2518903", + "Arn" + ] + }, + "integrationMethod": "POST", + "integrationType": "AWS", + "integrationUri": { + "Fn::Join": [ + "", + [ + "arn:aws:apigateway:", + { + "Ref": "AWS::Region" + }, + ":dynamodb:action/PutItem" + ] + ] + }, + "requestTemplates": { + "application/json": { + "Fn::Join": [ + "", + [ + "{\"TableName\":\"", + { + "Ref": "MyTable794EDED1" + }, + "\",\"Item\":{\"id\":{\"S\":\"$context.requestId\"}}}" + ] + ] + } + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_apigatewayv2.CfnIntegration", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_apigatewayv2.WebSocketIntegration", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "integ-aws-websocket-integration/mywsapi/$connect-Route/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::Route", + "aws:cdk:cloudformation:props": { + "apiId": { + "Ref": "mywsapi32E6CE11" + }, + "authorizationType": "NONE", + "routeKey": "$connect", + "target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mywsapiconnectRouteDynamodbPutItem9E189A39" + } + ] + ] + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_apigatewayv2.CfnRoute", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_apigatewayv2.WebSocketRoute", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_apigatewayv2.WebSocketApi", + "version": "0.0.0" + } + }, + "DevStage": { + "id": "DevStage", + "path": "integ-aws-websocket-integration/DevStage", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-aws-websocket-integration/DevStage/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::Stage", + "aws:cdk:cloudformation:props": { + "apiId": { + "Ref": "mywsapi32E6CE11" + }, + "autoDeploy": true, + "stageName": "dev" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_apigatewayv2.CfnStage", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_apigatewayv2.WebSocketStage", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "integ-aws-websocket-integration/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "integ-aws-websocket-integration/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + }, + "apigatewayv2-aws-integration-integ-test": { + "id": "apigatewayv2-aws-integration-integ-test", + "path": "apigatewayv2-aws-integration-integ-test", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "apigatewayv2-aws-integration-integ-test/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "apigatewayv2-aws-integration-integ-test/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "apigatewayv2-aws-integration-integ-test/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "apigatewayv2-aws-integration-integ-test/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "apigatewayv2-aws-integration-integ-test/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.ts new file mode 100644 index 0000000000000..222d45b70f179 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.ts @@ -0,0 +1,67 @@ +import { HttpMethod, WebSocketApi, WebSocketStage } from 'aws-cdk-lib/aws-apigatewayv2'; +import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import { App, RemovalPolicy, Stack } from 'aws-cdk-lib'; +import { WebSocketAwsIntegration, WebSocketMockIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations'; +import { IntegTest } from '@aws-cdk/integ-tests-alpha'; + +/* + * Stack verification steps: + * 1. Verify manually that the integration has type "MOCK" + */ + +const app = new App(); +const stack = new Stack(app, 'integ-aws-websocket-integration'); + +const table = new dynamodb.Table(stack, 'MyTable', { + tableName: 'MyTable', + partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }, + billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, + removalPolicy: RemovalPolicy.DESTROY, +}); + +// Create an IAM role for API Gateway +const apiRole = new iam.Role(stack, 'ApiGatewayRole', { + assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), + managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonDynamoDBFullAccess')], +}); + +const webSocketApi = new WebSocketApi(stack, 'mywsapi', { + defaultRouteOptions: { integration: new WebSocketMockIntegration('DefaultIntegration') }, +}); + +// Optionally, create a WebSocket stage +new WebSocketStage(stack, 'DevStage', { + webSocketApi: webSocketApi, + stageName: 'dev', + autoDeploy: true, +}); + +webSocketApi.addRoute('$connect', { + integration: new WebSocketAwsIntegration('DynamodbPutItem', { + integrationUri: `arn:aws:apigateway:${stack.region}:dynamodb:action/PutItem`, + integrationMethod: HttpMethod.POST, + credentialsRole: apiRole, + requestTemplates: { + 'application/json': JSON.stringify({ + TableName: table.tableName, + Item: { + id: { + S: '$context.requestId', + }, + }, + }), + }, + }), +}); + +new IntegTest(app, 'apigatewayv2-aws-integration-integ-test', { + testCases: [stack], + cdkCommandOptions: { + deploy: { + args: { + rollback: true, + }, + }, + }, +}); diff --git a/packages/aws-cdk-lib/aws-apigatewayv2-integrations/README.md b/packages/aws-cdk-lib/aws-apigatewayv2-integrations/README.md index ff18c4716a379..ba8d08d880d55 100644 --- a/packages/aws-cdk-lib/aws-apigatewayv2-integrations/README.md +++ b/packages/aws-cdk-lib/aws-apigatewayv2-integrations/README.md @@ -9,6 +9,7 @@ - [Request Parameters](#request-parameters) - [WebSocket APIs](#websocket-apis) - [Lambda WebSocket Integration](#lambda-websocket-integration) + - [AWS WebSocket Integration](#aws-websocket-integration) ## HTTP APIs @@ -210,3 +211,44 @@ webSocketApi.addRoute('sendMessage', { integration: new WebSocketLambdaIntegration('SendMessageIntegration', messageHandler), }); ``` + +### AWS WebSocket Integration + +AWS type integrations enable integrating with any supported AWS service. This is only supported for WebSocket APIs. When a client +connects/disconnects or sends a message specific to a route, the API Gateway service forwards the request to the specified AWS service. + +The following code configures a `$connect` route with a AWS integration that integrates with a dynamodb table. On websocket api connect, +it will write new entry to the dynamodb table. + +```ts +import { WebSocketAwsIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations'; +import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; +import * as iam from 'aws-cdk-lib/aws-iam'; + +const webSocketApi = new apigwv2.WebSocketApi(this, 'mywsapi'); +new apigwv2.WebSocketStage(this, 'mystage', { + webSocketApi, + stageName: 'dev', + autoDeploy: true, +}); + +declare const apiRole: iam.Role; +declare const table: dynamodb.Table; +webSocketApi.addRoute('$connect', { + integration: new WebSocketAwsIntegration('DynamodbPutItem', { + integrationUri: `arn:aws:apigateway:${this.region}:dynamodb:action/PutItem`, + integrationMethod: apigwv2.HttpMethod.POST, + credentialsRole: apiRole, + requestTemplates: { + 'application/json': JSON.stringify({ + TableName: table.tableName, + Item: { + id: { + S: '$context.requestId', + }, + }, + }), + }, + }), +}); +``` \ No newline at end of file diff --git a/packages/aws-cdk-lib/aws-apigatewayv2-integrations/lib/websocket/aws.ts b/packages/aws-cdk-lib/aws-apigatewayv2-integrations/lib/websocket/aws.ts new file mode 100644 index 0000000000000..6d7654480b8b4 --- /dev/null +++ b/packages/aws-cdk-lib/aws-apigatewayv2-integrations/lib/websocket/aws.ts @@ -0,0 +1,80 @@ +import { + WebSocketRouteIntegration, + WebSocketIntegrationType, + WebSocketRouteIntegrationConfig, + WebSocketRouteIntegrationBindOptions, +} from '../../../aws-apigatewayv2'; +import { IRole } from '../../../aws-iam'; + +/** + * Props for AWS type integration for an HTTP Api. + */ +export interface WebSocketAwsIntegrationProps { + /** + * Integration URI. + */ + readonly integrationUri: string; + + /** + * Specifies the integration's HTTP method type. + */ + readonly integrationMethod: string; + + /** + * Specifies the credentials role required for the integration. + * + * @default - No credential role provided. + */ + readonly credentialsRole?: IRole; + + /** + * The request parameters that API Gateway sends with the backend request. + * Specify request parameters as key-value pairs (string-to-string + * mappings), with a destination as the key and a source as the value. + * + * @default - No request parameter provided to the integration. + */ + readonly requestParameters?: { [dest: string]: string }; + + /** + * A map of Apache Velocity templates that are applied on the request + * payload. + * + * ``` + * { "application/json": "{ \"statusCode\": 200 }" } + * ``` + * + * @default - No request template provided to the integration. + */ + readonly requestTemplates?: { [contentType: string]: string }; + + /** + * The template selection expression for the integration. + * + * @default - No template selection expression provided. + */ + readonly templateSelectionExpression?: string; +} + +/** + * AWS WebSocket AWS Type Integration + */ +export class WebSocketAwsIntegration extends WebSocketRouteIntegration { + /** + * @param id id of the underlying integration construct + */ + constructor(id: string, private readonly props: WebSocketAwsIntegrationProps) { + super(id); + } + + bind(_options: WebSocketRouteIntegrationBindOptions): WebSocketRouteIntegrationConfig { + return { + type: WebSocketIntegrationType.AWS, + uri: this.props.integrationUri, + method: this.props.integrationMethod, + credentialsRole: this.props.credentialsRole, + requestTemplates: this.props.requestTemplates, + templateSelectionExpression: this.props.templateSelectionExpression, + }; + } +} diff --git a/packages/aws-cdk-lib/aws-apigatewayv2-integrations/lib/websocket/index.ts b/packages/aws-cdk-lib/aws-apigatewayv2-integrations/lib/websocket/index.ts index 9c6035e3957d4..28e1445dc94f8 100644 --- a/packages/aws-cdk-lib/aws-apigatewayv2-integrations/lib/websocket/index.ts +++ b/packages/aws-cdk-lib/aws-apigatewayv2-integrations/lib/websocket/index.ts @@ -1,2 +1,3 @@ export * from './lambda'; export * from './mock'; +export * from './aws'; diff --git a/packages/aws-cdk-lib/aws-apigatewayv2-integrations/test/websocket/aws.test.ts b/packages/aws-cdk-lib/aws-apigatewayv2-integrations/test/websocket/aws.test.ts new file mode 100644 index 0000000000000..4bdbf544e2c03 --- /dev/null +++ b/packages/aws-cdk-lib/aws-apigatewayv2-integrations/test/websocket/aws.test.ts @@ -0,0 +1,28 @@ +import { WebSocketAwsIntegration } from './../../lib/websocket/aws'; +import { Template } from '../../../assertions'; +import { WebSocketApi } from '../../../aws-apigatewayv2'; +import { Stack } from '../../../core'; + +describe('MockWebSocketIntegration', () => { + test('default', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new WebSocketApi(stack, 'Api', { + defaultRouteOptions: { + integration: new WebSocketAwsIntegration('AwsIntegration', { + integrationUri: 'arn:aws:apigateway:us-west-2:dynamodb:action/PutItem', + integrationMethod: 'POST', + }), + }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'AWS', + IntegrationUri: 'arn:aws:apigateway:us-west-2:dynamodb:action/PutItem', + IntegrationMethod: 'POST', + }); + }); +}); diff --git a/packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/integration.ts b/packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/integration.ts index 9e1df2aa64ae6..0eeeeaa97a927 100644 --- a/packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/integration.ts +++ b/packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/integration.ts @@ -2,6 +2,7 @@ import { Construct } from 'constructs'; import { IWebSocketApi } from './api'; import { IWebSocketRoute } from './route'; import { CfnIntegration } from '.././index'; +import { IRole } from '../../../aws-iam'; import { Resource } from '../../../core'; import { IIntegration } from '../common'; @@ -24,7 +25,11 @@ export enum WebSocketIntegrationType { /** * Mock Integration Type */ - MOCK = 'MOCK' + MOCK = 'MOCK', + /** + * AWS Integration Type + */ + AWS = 'AWS', } /** @@ -45,6 +50,48 @@ export interface WebSocketIntegrationProps { * Integration URI. */ readonly integrationUri: string; + + /** + * Specifies the integration's HTTP method type. + * + * @default - No HTTP method required. + */ + readonly integrationMethod?: string; + + /** + * Specifies the IAM role required for the integration. + * + * @default - No IAM role required. + */ + readonly credentialsRole?: IRole; + + /** + * The request parameters that API Gateway sends with the backend request. + * Specify request parameters as key-value pairs (string-to-string + * mappings), with a destination as the key and a source as the value. + * + * @default - No request parameters required. + */ + readonly requestParameters?: { [dest: string]: string }; + + /** + * A map of Apache Velocity templates that are applied on the request + * payload. + * + * ``` + * { "application/json": "{ \"statusCode\": 200 }" } + * ``` + * + * @default - No request templates required. + */ + readonly requestTemplates?: { [contentType: string]: string }; + + /** + * The template selection expression for the integration. + * + * @default - No template selection expression required. + */ + readonly templateSelectionExpression?: string; } /** @@ -61,6 +108,11 @@ export class WebSocketIntegration extends Resource implements IWebSocketIntegrat apiId: props.webSocketApi.apiId, integrationType: props.integrationType, integrationUri: props.integrationUri, + integrationMethod: props.integrationMethod, + credentialsArn: props.credentialsRole?.roleArn, + requestParameters: props.requestParameters, + requestTemplates: props.requestTemplates, + templateSelectionExpression: props.templateSelectionExpression, }); this.integrationId = integ.ref; this.webSocketApi = props.webSocketApi; @@ -112,6 +164,11 @@ export abstract class WebSocketRouteIntegration { webSocketApi: options.route.webSocketApi, integrationType: config.type, integrationUri: config.uri, + integrationMethod: config.method, + credentialsRole: config.credentialsRole, + requestTemplates: config.requestTemplates, + requestParameters: config.requestParameters, + templateSelectionExpression: config.templateSelectionExpression, }); } @@ -137,4 +194,39 @@ export interface WebSocketRouteIntegrationConfig { * Integration URI */ readonly uri: string; + + /** + * Integration method + * + * @default - No integration method. + */ + readonly method?: string; + + /** + * Credentials role + * + * @default - No role provided. + */ + readonly credentialsRole?: IRole; + + /** + * Request template + * + * @default - No request template provided. + */ + readonly requestTemplates?: { [contentType: string]: string }; + + /** + * Request parameters + * + * @default - No request parameters provided. + */ + readonly requestParameters?: { [dest: string]: string }; + + /** + * Template selection expression + * + * @default - No template selection expression. + */ + readonly templateSelectionExpression?: string; }