From 6c4d7cfcc7aa0243f202a9e200f27c0e2bdd54ff Mon Sep 17 00:00:00 2001 From: Daniel Mil <84205762+mildaniel@users.noreply.github.com> Date: Thu, 27 Jul 2023 09:50:18 -0700 Subject: [PATCH] feat: Link Gateway V2 APIs to Lambda Functions (#5626) * feat: Link gateway v2 apis to lambda functions * Fix typo * Format files --- .../terraform/hooks/prepare/exceptions.py | 12 +++ .../hooks/prepare/resource_linking.py | 78 +++++++++++++++++++ .../hooks/prepare/resources/resource_links.py | 6 ++ .../hooks/prepare/test_resource_linking.py | 78 +++++++++++++++++++ 4 files changed, 174 insertions(+) diff --git a/samcli/hook_packages/terraform/hooks/prepare/exceptions.py b/samcli/hook_packages/terraform/hooks/prepare/exceptions.py index 10ee377a0b..b04ee46304 100644 --- a/samcli/hook_packages/terraform/hooks/prepare/exceptions.py +++ b/samcli/hook_packages/terraform/hooks/prepare/exceptions.py @@ -287,6 +287,18 @@ class GatewayV2RouteToGatewayV2ApiLocalVariablesLinkingLimitationException(Local """ +class OneGatewayV2ApiToLambdaFunctionLinkingLimitationException(OneResourceLinkingLimitationException): + """ + Exception specific for Gateway V2 API linking to more than one Lambda Function + """ + + +class GatewayV2ApiToLambdaFunctionLocalVariablesLinkingLimitationException(LocalVariablesLinkingLimitationException): + """ + Exception specific for Gateway V2 API linking to Lambda Function using locals. + """ + + class OneGatewayV2StageToGatewayV2ApiLinkingLimitationException(OneResourceLinkingLimitationException): """ Exception specific for Gateway V2 Stage linking to more than one Gateway V2 API diff --git a/samcli/hook_packages/terraform/hooks/prepare/resource_linking.py b/samcli/hook_packages/terraform/hooks/prepare/resource_linking.py index 11b7a2b539..6b1ee3ffad 100644 --- a/samcli/hook_packages/terraform/hooks/prepare/resource_linking.py +++ b/samcli/hook_packages/terraform/hooks/prepare/resource_linking.py @@ -16,6 +16,7 @@ GatewayResourceToApiGatewayIntegrationResponseLocalVariablesLinkingLimitationException, GatewayResourceToApiGatewayMethodLocalVariablesLinkingLimitationException, GatewayResourceToGatewayRestApiLocalVariablesLinkingLimitationException, + GatewayV2ApiToLambdaFunctionLocalVariablesLinkingLimitationException, GatewayV2IntegrationToGatewayV2ApiLocalVariablesLinkingLimitationException, GatewayV2IntegrationToLambdaFunctionLocalVariablesLinkingLimitationException, GatewayV2RouteToGatewayV2ApiLocalVariablesLinkingLimitationException, @@ -31,6 +32,7 @@ OneGatewayResourceToApiGatewayIntegrationResponseLinkingLimitationException, OneGatewayResourceToApiGatewayMethodLinkingLimitationException, OneGatewayResourceToRestApiLinkingLimitationException, + OneGatewayV2ApiToLambdaFunctionLinkingLimitationException, OneGatewayV2IntegrationToGatewayV2ApiLinkingLimitationException, OneGatewayV2IntegrationToLambdaFunctionLinkingLimitationException, OneGatewayV2RouteToGatewayV2ApiLinkingLimitationException, @@ -2006,6 +2008,82 @@ def _link_gateway_v2_route_to_api( ResourceLinker(resource_linking_pair).link_resources() +def _link_gateway_v2_api_to_function_callback( + gateway_v2_api_cfn_resource: Dict, referenced_function_resource_values: List[ReferenceType] +) -> None: + """ + Callback function that is used by the linking algorithm to update an Api Gateway V2 API CFN Resource with + a reference to the Lambda function resource through the AWS_PROXY integration. + + Parameters + ---------- + gateway_v2_api_cfn_resource: Dict + API Gateway V2 API CFN resource + referenced_function_resource_values: List[ReferenceType] + List of referenced Gateway Resources either as the logical id of Lambda function resource + defined in the customer project, or ARN values for actual Lambda function resource defined + in customer's account. This list should always contain one element only. + """ + if len(referenced_function_resource_values) > 1: + raise InvalidResourceLinkingException("Could not link a V2 API to more than one Lambda Function resources") + + if not referenced_function_resource_values: + LOG.info("Unable to find any references to Lambda functions, skip linking Lambda function to Gateway V2 API") + return + + logical_id = referenced_function_resource_values[0] + gateway_v2_api_cfn_resource["Properties"]["Target"] = ( + {"Fn::Sub": INVOKE_ARN_FORMAT.format(function_logical_id=logical_id.value)} + if isinstance(logical_id, LogicalIdReference) + else logical_id.value + ) + + +def _link_gateway_v2_api_to_function( + gateway_api_config_resources: Dict[str, TFResource], + gateway_api_config_address_cfn_resources_map: Dict[str, List], + lambda_function_resources: Dict[str, Dict], +) -> None: + """ + Iterate through all the resources and link the corresponding + Gateway V2 API resources to each Lambda Function + + Parameters + ---------- + gateway_api_config_resources: Dict[str, TFResource] + Dictionary of configuration Gateway APIs + gateway_api_config_address_cfn_resources_map: Dict[str, List] + Dictionary containing resolved configuration addresses matched up to the cfn Gateway API + lambda_function_resources: Dict[str, Dict] + Dictionary of all Terraform Lambda Function resources (not configuration resources). + The dictionary's key is the calculated logical id for each resource. + """ + + # Only link APIs to resources if they are "Quick Create" APIs + quick_create_api_config_resources = { + config_address: tf_resource + for config_address, tf_resource in gateway_api_config_resources.items() + if "target" in tf_resource.attributes + } + + exceptions = ResourcePairExceptions( + multiple_resource_linking_exception=OneGatewayV2ApiToLambdaFunctionLinkingLimitationException, + local_variable_linking_exception=GatewayV2ApiToLambdaFunctionLocalVariablesLinkingLimitationException, + ) + resource_linking_pair = ResourceLinkingPair( + source_resource_cfn_resource=gateway_api_config_address_cfn_resources_map, + source_resource_tf_config=quick_create_api_config_resources, + destination_resource_tf=lambda_function_resources, + tf_destination_attribute_name="invoke_arn", + terraform_link_field_name="target", + cfn_link_field_name="Target", + terraform_resource_type_prefix=LAMBDA_FUNCTION_RESOURCE_ADDRESS_PREFIX, + cfn_resource_update_call_back_function=_link_gateway_v2_api_to_function_callback, + linking_exceptions=exceptions, + ) + ResourceLinker(resource_linking_pair).link_resources() + + def _link_gateway_v2_stage_to_api( gateway_stage_config_resources: Dict[str, TFResource], gateway_stage_config_address_cfn_resources_map: Dict[str, List], diff --git a/samcli/hook_packages/terraform/hooks/prepare/resources/resource_links.py b/samcli/hook_packages/terraform/hooks/prepare/resources/resource_links.py index 5e8056c697..fe940afb8a 100644 --- a/samcli/hook_packages/terraform/hooks/prepare/resources/resource_links.py +++ b/samcli/hook_packages/terraform/hooks/prepare/resources/resource_links.py @@ -28,6 +28,7 @@ _link_gateway_methods_to_gateway_rest_apis, _link_gateway_resources_to_gateway_rest_apis, _link_gateway_stage_to_rest_api, + _link_gateway_v2_api_to_function, _link_gateway_v2_integration_to_api, _link_gateway_v2_integration_to_lambda_function, _link_gateway_v2_route_to_api, @@ -119,6 +120,11 @@ dest=TF_AWS_API_GATEWAY_V2_API, linking_func=_link_gateway_v2_route_to_api, ), + LinkingPairCaller( + source=TF_AWS_API_GATEWAY_V2_API, + dest=TF_AWS_LAMBDA_FUNCTION, + linking_func=_link_gateway_v2_api_to_function, + ), LinkingPairCaller( source=TF_AWS_API_GATEWAY_V2_STAGE, dest=TF_AWS_API_GATEWAY_V2_API, diff --git a/tests/unit/hook_packages/terraform/hooks/prepare/test_resource_linking.py b/tests/unit/hook_packages/terraform/hooks/prepare/test_resource_linking.py index 48d6dc183f..7dfa506ec9 100644 --- a/tests/unit/hook_packages/terraform/hooks/prepare/test_resource_linking.py +++ b/tests/unit/hook_packages/terraform/hooks/prepare/test_resource_linking.py @@ -45,6 +45,8 @@ GatewayV2IntegrationToGatewayV2ApiLocalVariablesLinkingLimitationException, OneGatewayV2RouteToGatewayV2ApiLinkingLimitationException, GatewayV2RouteToGatewayV2ApiLocalVariablesLinkingLimitationException, + OneGatewayV2ApiToLambdaFunctionLinkingLimitationException, + GatewayV2ApiToLambdaFunctionLocalVariablesLinkingLimitationException, OneGatewayV2StageToGatewayV2ApiLinkingLimitationException, GatewayV2StageToGatewayV2ApiLocalVariablesLinkingLimitationException, ) @@ -101,6 +103,8 @@ API_GATEWAY_V2_API_RESOURCE_ADDRESS_PREFIX, _link_gateway_v2_resource_to_api_callback, _link_gateway_v2_route_to_api, + _link_gateway_v2_api_to_function, + _link_gateway_v2_api_to_function_callback, _link_gateway_v2_stage_to_api, ) from samcli.hook_packages.terraform.hooks.prepare.utilities import get_configuration_address @@ -2141,6 +2145,10 @@ def test_link_gateway_integration_to_function_call_back( _link_gateway_v2_resource_to_api_callback, "Could not link multiple Gateway V2 Apis to one Gateway V2 resource", ), + ( + _link_gateway_v2_api_to_function_callback, + "Could not link a V2 API to more than one Lambda Function resources", + ), ] ) def test_linking_callbacks_raises_multiple_reference_exception(self, linking_call_back_method, expected_message): @@ -2158,6 +2166,7 @@ def test_linking_callbacks_raises_multiple_reference_exception(self, linking_cal (_link_gateway_v2_route_to_integration_callback,), (_link_gateway_v2_integration_to_lambda_function_callback,), (_link_gateway_v2_resource_to_api_callback,), + (_link_gateway_v2_api_to_function_callback,), ] ) def test_linking_callbacks_skips_empty_references(self, linking_call_back_method): @@ -2727,3 +2736,72 @@ def test_link_gateway_v2_integration_to_api_callback( _link_gateway_v2_resource_to_api_callback(gateway_resource, logical_ids) input_gateway_v2_integration["Properties"]["ApiId"] = expected_api_reference self.assertEqual(gateway_resource, input_gateway_v2_integration) + + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking._link_gateway_v2_api_to_function_callback") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourceLinker") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourceLinkingPair") + @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourcePairExceptions") + def test_link_gateway_v2_api_to_lambda_function( + self, + mock_resource_linking_exceptions, + mock_resource_linking_pair, + mock_resource_linker, + mock_link_gateway_v2_api_to_function_callback, + ): + api_v2_cfn_resources = Mock() + quick_create_resource = TFResource("resource_address", "type", Mock(), {"target": ConstantValue("val")}) + combined_resources = { + "ResourceA": quick_create_resource, + "ResourceB": TFResource("resource_address", "type", Mock(), {"name": ConstantValue("MyAPI")}), + } + expected_quick_create_resource = {"ResourceA": quick_create_resource} + lambda_function_tf_resources = Mock() + + _link_gateway_v2_api_to_function(combined_resources, api_v2_cfn_resources, lambda_function_tf_resources) + + mock_resource_linking_exceptions.assert_called_once_with( + multiple_resource_linking_exception=OneGatewayV2ApiToLambdaFunctionLinkingLimitationException, + local_variable_linking_exception=GatewayV2ApiToLambdaFunctionLocalVariablesLinkingLimitationException, + ) + + mock_resource_linking_pair.assert_called_once_with( + source_resource_cfn_resource=api_v2_cfn_resources, + source_resource_tf_config=expected_quick_create_resource, + destination_resource_tf=lambda_function_tf_resources, + tf_destination_attribute_name="invoke_arn", + terraform_link_field_name="target", + cfn_link_field_name="Target", + terraform_resource_type_prefix=LAMBDA_FUNCTION_RESOURCE_ADDRESS_PREFIX, + cfn_resource_update_call_back_function=mock_link_gateway_v2_api_to_function_callback, + linking_exceptions=mock_resource_linking_exceptions(), + ) + + mock_resource_linker.assert_called_once_with(mock_resource_linking_pair()) + + @parameterized.expand( + [ + ( + { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": {"Target": "functionA.invoke_arn"}, + }, + [LogicalIdReference("FunctionA")], + { + "Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${FunctionA.Arn}/invocations" + }, + ), + ( + { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": {"Target": "functionA.invoke_arn"}, + }, + [ExistingResourceReference("myapi_arn")], + "myapi_arn", + ), + ] + ) + def test_link_gateway_v2_api_to_function_callback(self, input_gateway_v2_api, logical_ids, expected_api_reference): + gateway_resource = deepcopy(input_gateway_v2_api) + _link_gateway_v2_api_to_function_callback(gateway_resource, logical_ids) + input_gateway_v2_api["Properties"]["Target"] = expected_api_reference + self.assertEqual(gateway_resource, input_gateway_v2_api)