Skip to content

Commit d6a9d8d

Browse files
committed
Merge branch 'eaglepointpartners/master' into master
PR #1928 * eaglepointpartners/master: Add changelog for tf websocket support Revert formating changes Use function name as output for handler names Fixed prchecks (#1) Added tests for websockets_api terraform packaging Removed incorrect comment from TerraformGenerator Fixed terraform package websockets lambda invoke permission resource definition Fixed integration_uri Adds terraform packaging support for websocketapi
2 parents 8e51ee6 + 896f1aa commit d6a9d8d

File tree

3 files changed

+368
-20
lines changed

3 files changed

+368
-20
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "enhancement",
3+
"category": "Websockets",
4+
"description": "Add support for WebSockets API Terraform packaging (#1670)"
5+
}

chalice/package.py

Lines changed: 197 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from chalice.utils import (
1919
OSUtils, UI, serialize_to_json, to_cfn_resource_name
2020
)
21-
from chalice.awsclient import TypedAWSClient # noqa
21+
from chalice.awsclient import TypedAWSClient # noqa
2222
from chalice.config import Config # noqa
2323
from chalice.deploy import models
2424
from chalice.deploy.appgraph import ApplicationGraphBuilder, DependencyBuilder
@@ -122,7 +122,6 @@ def construct_resources(self, config, chalice_stage_name):
122122

123123

124124
class TemplateGenerator(object):
125-
126125
template_file = None # type: str
127126

128127
def __init__(self, config, options):
@@ -858,17 +857,202 @@ def _generate_managediamrole(self, resource, template):
858857
'role': '${aws_iam_role.%s.id}' % resource.resource_name,
859858
}
860859

860+
def _add_websocket_lambda_integration(
861+
self, websocket_api_id, websocket_handler, template):
862+
# type: (str, str, Dict[str, Any]) -> None
863+
websocket_handler_function_name = \
864+
"${aws_lambda_function.%s.function_name}" % websocket_handler
865+
resource_definition = {
866+
'api_id': websocket_api_id,
867+
'connection_type': 'INTERNET',
868+
'content_handling_strategy': 'CONVERT_TO_TEXT',
869+
'integration_type': 'AWS_PROXY',
870+
'integration_uri': self._arnref(
871+
"arn:%(partition)s:apigateway:%(region)s"
872+
":lambda:path/2015-03-31/functions/arn"
873+
":%(partition)s:lambda:%(region)s"
874+
":%(account_id)s:function"
875+
":%(websocket_handler_function_name)s/invocations",
876+
websocket_handler_function_name=websocket_handler_function_name
877+
)
878+
}
879+
template['resource'].setdefault(
880+
'aws_apigatewayv2_integration', {}
881+
)['%s_api_integration' % websocket_handler] = resource_definition
882+
883+
def _add_websocket_lambda_invoke_permission(
884+
self, websocket_api_id, websocket_handler, template):
885+
# type: (str, str, Dict[str, Any]) -> None
886+
websocket_handler_function_name = \
887+
"${aws_lambda_function.%s.function_name}" % websocket_handler
888+
resource_definition = {
889+
"function_name": websocket_handler_function_name,
890+
"action": "lambda:InvokeFunction",
891+
"principal": self._options.service_principal('apigateway'),
892+
"source_arn": self._arnref(
893+
"arn:%(partition)s:execute-api"
894+
":%(region)s:%(account_id)s"
895+
":%(websocket_api_id)s/*",
896+
websocket_api_id=websocket_api_id
897+
)
898+
}
899+
template['resource'].setdefault(
900+
'aws_lambda_permission', {}
901+
)['%s_invoke_permission' % websocket_handler] = resource_definition
902+
903+
def _add_websockets_route(self, websocket_api_id, route_key, template):
904+
# type: (str, str, Dict[str, Any]) -> str
905+
integration_target = {
906+
'$connect': 'integrations/${aws_apigatewayv2_integration'
907+
'.websocket_connect_api_integration.id}',
908+
'$disconnect': 'integrations/${aws_apigatewayv2_integration'
909+
'.websocket_disconnect_api_integration.id}',
910+
}.get(route_key,
911+
'integrations/${aws_apigatewayv2_integration'
912+
'.websocket_message_api_integration.id}')
913+
914+
route_resource_name = {
915+
'$connect': 'websocket_connect_route',
916+
'$disconnect': 'websocket_disconnect_route',
917+
'$default': 'websocket_message_route',
918+
}.get(route_key, 'message')
919+
920+
template['resource'].setdefault(
921+
'aws_apigatewayv2_route', {}
922+
)[route_resource_name] = {
923+
"api_id": websocket_api_id,
924+
"route_key": route_key,
925+
"target": integration_target
926+
}
927+
return route_resource_name
928+
929+
def _add_websocket_domain_name(self, websocket_api_id, resource, template):
930+
# type: (str, models.WebsocketAPI, Dict[str, Any]) -> None
931+
if resource.domain_name is None:
932+
return
933+
domain_name = resource.domain_name
934+
935+
ws_domain_name_definition = {
936+
"domain_name": domain_name.domain_name,
937+
"domain_name_configuration": {
938+
'certificate_arn': domain_name.certificate_arn,
939+
'endpoint_type': 'REGIONAL',
940+
},
941+
}
942+
943+
if domain_name.tags:
944+
ws_domain_name_definition['tags'] = domain_name.tags
945+
946+
template['resource'].setdefault(
947+
'aws_apigatewayv2_domain_name', {}
948+
)[domain_name.resource_name] = ws_domain_name_definition
949+
950+
template['resource'].setdefault(
951+
'aws_apigatewayv2_api_mapping', {}
952+
)[domain_name.resource_name + '_mapping'] = {
953+
"api_id": websocket_api_id,
954+
"domain_name": "${aws_apigatewayv2_domain_name.%s.id}" %
955+
domain_name.resource_name,
956+
"stage": "${aws_apigatewayv2_stage.websocket_api_stage.id}",
957+
}
958+
959+
def _inject_websocketapi_outputs(self, websocket_api_id, template):
960+
# type: (str, Dict[str, Any]) -> None
961+
aws_lambda_functions = template['resource']['aws_lambda_function']
962+
stage_name = \
963+
template['resource']['aws_apigatewayv2_stage'][
964+
'websocket_api_stage'][
965+
'name']
966+
output = template.setdefault('output', {})
967+
output['WebsocketAPIId'] = {"value": websocket_api_id}
968+
969+
if 'websocket_connect' in aws_lambda_functions:
970+
output['WebsocketConnectHandlerArn'] = {
971+
"value": "${aws_lambda_function.websocket_connect.arn}"}
972+
output['WebsocketConnectHandlerName'] = {
973+
"value": (
974+
"${aws_lambda_function.websocket_connect.function_name}")}
975+
if 'websocket_message' in aws_lambda_functions:
976+
output['WebsocketMessageHandlerArn'] = {
977+
"value": "${aws_lambda_function.websocket_message.arn}"}
978+
output['WebsocketMessageHandlerName'] = {
979+
"value": (
980+
"${aws_lambda_function.websocket_message.function_name}")}
981+
if 'websocket_disconnect' in aws_lambda_functions:
982+
output['WebsocketDisconnectHandlerArn'] = {
983+
"value": "${aws_lambda_function.websocket_disconnect.arn}"}
984+
output['WebsocketDisconnectHandlerName'] = {
985+
"value": (
986+
"${aws_lambda_function.websocket_disconnect"
987+
".function_name}")}
988+
989+
output['WebsocketConnectEndpointURL'] = {
990+
"value": (
991+
'wss://%(websocket_api_id)s.execute-api'
992+
# The api_gateway_stage is filled in when
993+
# the template is built.
994+
'.${data.aws_region.chalice.name}'
995+
'.amazonaws.com/%(stage_name)s/'
996+
) % {
997+
"stage_name": stage_name,
998+
"websocket_api_id": websocket_api_id
999+
}
1000+
}
1001+
8611002
def _generate_websocketapi(self, resource, template):
8621003
# type: (models.WebsocketAPI, Dict[str, Any]) -> None
8631004

864-
message = (
865-
"Unable to package chalice apps that use experimental "
866-
"Websocket decorators. Terraform AWS Provider "
867-
"support for websocket is pending see "
868-
"https://git.io/fj9X8 for details and progress. "
869-
"You can deploy this app using `chalice deploy`."
870-
)
871-
raise NotImplementedError(message)
1005+
ws_definition = {
1006+
'name': resource.name,
1007+
'route_selection_expression': '$request.body.action',
1008+
'protocol_type': 'WEBSOCKET',
1009+
}
1010+
1011+
template['resource'].setdefault('aws_apigatewayv2_api', {})[
1012+
resource.resource_name] = ws_definition
1013+
1014+
websocket_api_id = "${aws_apigatewayv2_api.%s.id}" % \
1015+
resource.resource_name
1016+
1017+
websocket_handlers = [
1018+
'websocket_connect',
1019+
'websocket_message',
1020+
'websocket_disconnect',
1021+
]
1022+
1023+
for handler in websocket_handlers:
1024+
if handler in template['resource']['aws_lambda_function']:
1025+
self._add_websocket_lambda_integration(websocket_api_id,
1026+
handler, template)
1027+
self._add_websocket_lambda_invoke_permission(websocket_api_id,
1028+
handler, template)
1029+
1030+
route_resource_names = []
1031+
for route_key in resource.routes:
1032+
route_resource_name = self._add_websockets_route(websocket_api_id,
1033+
route_key,
1034+
template)
1035+
route_resource_names.append(route_resource_name)
1036+
1037+
template['resource'].setdefault(
1038+
'aws_apigatewayv2_deployment', {}
1039+
)['websocket_api_deployment'] = {
1040+
"api_id": websocket_api_id,
1041+
"depends_on": ["aws_apigatewayv2_route.%s" % name for name in
1042+
route_resource_names]
1043+
}
1044+
1045+
template['resource'].setdefault(
1046+
'aws_apigatewayv2_stage', {}
1047+
)['websocket_api_stage'] = {
1048+
"api_id": websocket_api_id,
1049+
"deployment_id": ("${aws_apigatewayv2_deployment"
1050+
".websocket_api_deployment.id}"),
1051+
"name": resource.api_gateway_stage
1052+
}
1053+
1054+
self._add_websocket_domain_name(websocket_api_id, resource, template)
1055+
self._inject_websocketapi_outputs(websocket_api_id, template)
8721056

8731057
def _generate_s3bucketnotification(self, resource, template):
8741058
# type: (models.S3BucketNotification, Dict[str, Any]) -> None
@@ -1026,9 +1210,9 @@ def _generate_lambdalayer(self, resource, template):
10261210
template['resource'].setdefault(
10271211
"aws_lambda_layer_version", {})[
10281212
resource.resource_name] = {
1029-
'layer_name': resource.layer_name,
1030-
'compatible_runtimes': [resource.runtime],
1031-
'filename': resource.deployment_package.filename,
1213+
'layer_name': resource.layer_name,
1214+
'compatible_runtimes': [resource.runtime],
1215+
'filename': resource.deployment_package.filename,
10321216
}
10331217
self._chalice_layer = resource.resource_name
10341218

0 commit comments

Comments
 (0)