From e490a2cf62be93d21bb1517cf4bf07a690c6e099 Mon Sep 17 00:00:00 2001 From: Mohamed ElAsmar Date: Thu, 3 Aug 2023 02:38:01 -0700 Subject: [PATCH] fix: link the API gateway resource parent to either rest api or another gateway resource --- .../terraform/hooks/prepare/constants.py | 10 + .../terraform/hooks/prepare/exceptions.py | 21 + .../hooks/prepare/property_builder.py | 24 +- .../hooks/prepare/resource_linking.py | 382 ++++++++++++------ .../hooks/prepare/resources/resource_links.py | 16 +- .../prepare/resources/resource_properties.py | 2 +- .../terraform/hooks/prepare/translate.py | 39 +- .../terraform/hooks/prepare/types.py | 6 + ...st_start_api_with_terraform_application.py | 38 +- .../terraform/terraform-v1-api-simple/main.tf | 40 +- .../HelloWorldFunction.zip | Bin 0 -> 1079 bytes .../terraform-v1-nested-apis/main.tf | 143 +++++++ .../resources/test_resource_properties.py | 5 +- .../hooks/prepare/test_property_builder.py | 2 +- .../hooks/prepare/test_resource_linking.py | 298 +++++++++----- .../terraform/hooks/prepare/test_translate.py | 49 ++- 16 files changed, 786 insertions(+), 289 deletions(-) create mode 100644 tests/integration/testdata/start_api/terraform/terraform-v1-nested-apis/HelloWorldFunction.zip create mode 100644 tests/integration/testdata/start_api/terraform/terraform-v1-nested-apis/main.tf diff --git a/samcli/hook_packages/terraform/hooks/prepare/constants.py b/samcli/hook_packages/terraform/hooks/prepare/constants.py index 6cbe591e57..ffde46e57c 100644 --- a/samcli/hook_packages/terraform/hooks/prepare/constants.py +++ b/samcli/hook_packages/terraform/hooks/prepare/constants.py @@ -13,3 +13,13 @@ CFN_AWS_LAMBDA_LAYER_VERSION: "Content", } COMPILED_REGULAR_EXPRESSION = re.compile(r"\[[^\[\]]*\]") +REMOTE_DUMMY_VALUE = "<>" +TF_AWS_LAMBDA_FUNCTION = "aws_lambda_function" +TF_AWS_LAMBDA_LAYER_VERSION = "aws_lambda_layer_version" +TF_AWS_API_GATEWAY_RESOURCE = "aws_api_gateway_resource" +TF_AWS_API_GATEWAY_REST_API = "aws_api_gateway_rest_api" +TF_AWS_API_GATEWAY_STAGE = "aws_api_gateway_stage" +TF_AWS_API_GATEWAY_METHOD = "aws_api_gateway_method" +TF_AWS_API_GATEWAY_INTEGRATION = "aws_api_gateway_integration" +TF_AWS_API_GATEWAY_AUTHORIZER = "aws_api_gateway_authorizer" +TF_AWS_API_GATEWAY_INTEGRATION_RESPONSE = "aws_api_gateway_method_response" diff --git a/samcli/hook_packages/terraform/hooks/prepare/exceptions.py b/samcli/hook_packages/terraform/hooks/prepare/exceptions.py index ca8cdf55f6..f0ea430478 100644 --- a/samcli/hook_packages/terraform/hooks/prepare/exceptions.py +++ b/samcli/hook_packages/terraform/hooks/prepare/exceptions.py @@ -19,6 +19,13 @@ def __init__(self, message): UserException.__init__(self, msg) +class UnexpectedDestinationResource(InvalidResourceLinkingException): + """ + Exception that will be thrown while doing terraform linking logic in case if the found destination resource is not + of the expected type, or the field used for linking is not the expected field. + """ + + class ApplyLimitationException(UserException): def __init__(self, message): fmt = "{message}{line_sep}{line_sep}{apply_work_around}" @@ -85,6 +92,20 @@ class GatewayResourceToGatewayRestApiLocalVariablesLinkingLimitationException(Lo """ +class OneGatewayResourceToParentResourceLinkingLimitationException(OneResourceLinkingLimitationException): + """ + Exception specific for Gateway Resource linking to more than one Parent Resources which is either Rest API or + Gateway Resource + """ + + +class GatewayResourceToParentResourceLocalVariablesLinkingLimitationException(LocalVariablesLinkingLimitationException): + """ + Exception specific for Gateway Resource linking to Parent Resources which is either Rest API or Gateway Resource + using locals. + """ + + class OneRestApiToApiGatewayMethodLinkingLimitationException(OneResourceLinkingLimitationException): """ Exception specific for Gateway Method linking to more than Rest API diff --git a/samcli/hook_packages/terraform/hooks/prepare/property_builder.py b/samcli/hook_packages/terraform/hooks/prepare/property_builder.py index a8910f112a..4fe50e935a 100644 --- a/samcli/hook_packages/terraform/hooks/prepare/property_builder.py +++ b/samcli/hook_packages/terraform/hooks/prepare/property_builder.py @@ -6,6 +6,18 @@ from json.decoder import JSONDecodeError from typing import Any, Dict, Optional +from samcli.hook_packages.terraform.hooks.prepare.constants import ( + REMOTE_DUMMY_VALUE, + TF_AWS_API_GATEWAY_AUTHORIZER, + TF_AWS_API_GATEWAY_INTEGRATION, + TF_AWS_API_GATEWAY_INTEGRATION_RESPONSE, + TF_AWS_API_GATEWAY_METHOD, + TF_AWS_API_GATEWAY_RESOURCE, + TF_AWS_API_GATEWAY_REST_API, + TF_AWS_API_GATEWAY_STAGE, + TF_AWS_LAMBDA_FUNCTION, + TF_AWS_LAMBDA_LAYER_VERSION, +) from samcli.hook_packages.terraform.hooks.prepare.resource_linking import _resolve_resource_attribute from samcli.hook_packages.terraform.hooks.prepare.resources.internal import ( INTERNAL_API_GATEWAY_INTEGRATION, @@ -29,18 +41,6 @@ LOG = logging.getLogger(__name__) -REMOTE_DUMMY_VALUE = "<>" -TF_AWS_LAMBDA_FUNCTION = "aws_lambda_function" -TF_AWS_LAMBDA_LAYER_VERSION = "aws_lambda_layer_version" - -TF_AWS_API_GATEWAY_RESOURCE = "aws_api_gateway_resource" -TF_AWS_API_GATEWAY_REST_API = "aws_api_gateway_rest_api" -TF_AWS_API_GATEWAY_STAGE = "aws_api_gateway_stage" -TF_AWS_API_GATEWAY_METHOD = "aws_api_gateway_method" -TF_AWS_API_GATEWAY_INTEGRATION = "aws_api_gateway_integration" -TF_AWS_API_GATEWAY_AUTHORIZER = "aws_api_gateway_authorizer" -TF_AWS_API_GATEWAY_INTEGRATION_RESPONSE = "aws_api_gateway_method_response" - def _build_code_property(tf_properties: dict, resource: TFResource) -> Any: """ diff --git a/samcli/hook_packages/terraform/hooks/prepare/resource_linking.py b/samcli/hook_packages/terraform/hooks/prepare/resource_linking.py index 80dd302d41..08ae51cff7 100644 --- a/samcli/hook_packages/terraform/hooks/prepare/resource_linking.py +++ b/samcli/hook_packages/terraform/hooks/prepare/resource_linking.py @@ -7,6 +7,7 @@ from dataclasses import dataclass from typing import Callable, Dict, List, Optional, Type, Union +from samcli.hook_packages.terraform.hooks.prepare.constants import TF_AWS_API_GATEWAY_REST_API from samcli.hook_packages.terraform.hooks.prepare.exceptions import ( FunctionLayerLocalVariablesLinkingLimitationException, GatewayAuthorizerToLambdaFunctionLocalVariablesLinkingLimitationException, @@ -16,6 +17,7 @@ GatewayResourceToApiGatewayIntegrationResponseLocalVariablesLinkingLimitationException, GatewayResourceToApiGatewayMethodLocalVariablesLinkingLimitationException, GatewayResourceToGatewayRestApiLocalVariablesLinkingLimitationException, + GatewayResourceToParentResourceLocalVariablesLinkingLimitationException, InvalidResourceLinkingException, LambdaFunctionToApiGatewayIntegrationLocalVariablesLinkingLimitationException, LocalVariablesLinkingLimitationException, @@ -25,6 +27,7 @@ OneGatewayResourceToApiGatewayIntegrationLinkingLimitationException, OneGatewayResourceToApiGatewayIntegrationResponseLinkingLimitationException, OneGatewayResourceToApiGatewayMethodLinkingLimitationException, + OneGatewayResourceToParentResourceLinkingLimitationException, OneGatewayResourceToRestApiLinkingLimitationException, OneLambdaFunctionResourceToApiGatewayIntegrationLinkingLimitationException, OneLambdaLayerLinkingLimitationException, @@ -37,6 +40,7 @@ RestApiToApiGatewayIntegrationResponseLocalVariablesLinkingLimitationException, RestApiToApiGatewayMethodLocalVariablesLinkingLimitationException, RestApiToApiGatewayStageLocalVariablesLinkingLimitationException, + UnexpectedDestinationResource, ) from samcli.hook_packages.terraform.hooks.prepare.resources.apigw import INVOKE_ARN_FORMAT from samcli.hook_packages.terraform.hooks.prepare.types import ( @@ -87,6 +91,7 @@ class LogicalIdReference(ReferenceType): for the destination resources defined in the customer TF project. """ + resource_type: str value: str @@ -96,15 +101,20 @@ class ResourcePairExceptions: local_variable_linking_exception: Type[LocalVariablesLinkingLimitationException] +@dataclass +class ResourcePairExceptedDestination: + terraform_resource_type_prefix: str + terraform_attribute_name: str + + @dataclass class ResourceLinkingPair: source_resource_cfn_resource: Dict[str, List] source_resource_tf_config: Dict[str, TFResource] destination_resource_tf: Dict[str, Dict] - tf_destination_attribute_name: str # arn or id + expected_destinations: List[ResourcePairExceptedDestination] terraform_link_field_name: str cfn_link_field_name: str - terraform_resource_type_prefix: str cfn_resource_update_call_back_function: Callable[[Dict, List[ReferenceType]], None] linking_exceptions: ResourcePairExceptions @@ -272,22 +282,36 @@ def _link_using_linking_fields(self, cfn_resource: Dict) -> None: values = [values] # build map between the destination linking field property values, and resources' logical ids + expected_destinations_map = { + expected_destination.terraform_resource_type_prefix: expected_destination.terraform_attribute_name + for expected_destination in self._resource_pair.expected_destinations + } child_resources_linking_attributes_logical_id_mapping = {} for logical_id, destination_resource in self._resource_pair.destination_resource_tf.items(): - linking_attribute_value = destination_resource.get("values", {}).get( - self._resource_pair.tf_destination_attribute_name - ) + destination_attribute = expected_destinations_map.get(f"{destination_resource.get('type', '')}.", "") + linking_attribute_value = destination_resource.get("values", {}).get(destination_attribute) if linking_attribute_value: - child_resources_linking_attributes_logical_id_mapping[linking_attribute_value] = logical_id + child_resources_linking_attributes_logical_id_mapping[linking_attribute_value] = ( + logical_id, + destination_resource.get("type", {}), + ) LOG.debug( - "The map between destination resources linking field %s, and resources logical ids is %s", - self._resource_pair.tf_destination_attribute_name, + "The map between destination resources linking fields %s, and resources logical ids is %s", + ", ".join( + [ + expected_destination.terraform_attribute_name + for expected_destination in self._resource_pair.expected_destinations + ] + ), child_resources_linking_attributes_logical_id_mapping, ) dest_resources = [ - LogicalIdReference(child_resources_linking_attributes_logical_id_mapping[value]) + LogicalIdReference( + value=child_resources_linking_attributes_logical_id_mapping[value][0], + resource_type=child_resources_linking_attributes_logical_id_mapping[value][1], + ) if value in child_resources_linking_attributes_logical_id_mapping else ExistingResourceReference(value) for value in values @@ -389,64 +413,74 @@ def _process_reference_resource_value( ) # Valid destination resource - if resolved_destination_resource.value.startswith(self._resource_pair.terraform_resource_type_prefix): - LOG.debug("Process the destination resource %s", resolved_destination_resource.value) - if not resolved_destination_resource.value.endswith(self._resource_pair.tf_destination_attribute_name): - LOG.debug( - "The used property in reference %s is not an ARN property", resolved_destination_resource.value - ) - raise InvalidResourceLinkingException( - f"Could not use the value {resolved_destination_resource.value} as a " - f"destination resource for the source resource " - f"{source_tf_resource.full_address}. The source resource " - f"value should refer to valid destination resource ARN property." - ) - - # we need to the resource name by removing the attribute part from the reference value - # as an example the reference will be look like aws_layer_version.layer1.arn - # and the attribute name is `arn`, we need to remove the last 4 characters `.arn` - # which is the length of the linking attribute `arn` in our example adding one for the `.` character - tf_dest_res_name = resolved_destination_resource.value[ - len(self._resource_pair.terraform_resource_type_prefix) : -len( - self._resource_pair.tf_destination_attribute_name - ) - - 1 - ] - if resolved_destination_resource.module_address: - tf_dest_resource_full_address = ( - f"{resolved_destination_resource.module_address}." - f"{self._resource_pair.terraform_resource_type_prefix}" - f"{tf_dest_res_name}" - ) - else: - tf_dest_resource_full_address = ( - f"{self._resource_pair.terraform_resource_type_prefix}{tf_dest_res_name}" - ) - cfn_dest_resource_logical_id = build_cfn_logical_id(tf_dest_resource_full_address) - LOG.debug( - "The logical id of the resource referred by %s is %s", - resolved_destination_resource.value, - cfn_dest_resource_logical_id, - ) - - # validate that the found dest resource is in mapped dest resources, which means that it is created. - # The resource can be defined in the TF plan configuration, but will not be created. - dest_resources: List[ReferenceType] = [] - if cfn_dest_resource_logical_id in self._resource_pair.destination_resource_tf: + for expected_destination in self._resource_pair.expected_destinations: + if resolved_destination_resource.value.startswith(expected_destination.terraform_resource_type_prefix): + LOG.debug("Process the destination resource %s", resolved_destination_resource.value) + if not resolved_destination_resource.value.endswith(expected_destination.terraform_attribute_name): + LOG.debug( + "The used property in reference %s is not an %s property", + resolved_destination_resource.value, + expected_destination.terraform_attribute_name, + ) + continue + + # we need to the resource name by removing the attribute part from the reference value + # as an example the reference will be look like aws_layer_version.layer1.arn + # and the attribute name is `arn`, we need to remove the last 4 characters `.arn` + # which is the length of the linking attribute `arn` in our example adding one for the `.` character + tf_dest_res_name = resolved_destination_resource.value[ + len(expected_destination.terraform_resource_type_prefix) : -len( + expected_destination.terraform_attribute_name + ) + - 1 + ] + if resolved_destination_resource.module_address: + tf_dest_resource_full_address = ( + f"{resolved_destination_resource.module_address}." + f"{expected_destination.terraform_resource_type_prefix}" + f"{tf_dest_res_name}" + ) + else: + tf_dest_resource_full_address = ( + f"{expected_destination.terraform_resource_type_prefix}{tf_dest_res_name}" + ) + cfn_dest_resource_logical_id = build_cfn_logical_id(tf_dest_resource_full_address) LOG.debug( - "The resource referred by %s can be found in the mapped destination resources", + "The logical id of the resource referred by %s is %s", resolved_destination_resource.value, + cfn_dest_resource_logical_id, ) - dest_resources.append(LogicalIdReference(cfn_dest_resource_logical_id)) - return dest_resources + + # validate that the found dest resource is in mapped dest resources, which means that it is created. + # The resource can be defined in the TF plan configuration, but will not be created. + dest_resources: List[ReferenceType] = [] + if cfn_dest_resource_logical_id in self._resource_pair.destination_resource_tf: + LOG.debug( + "The resource referred by %s can be found in the mapped destination resources", + resolved_destination_resource.value, + ) + dest_resources.append( + LogicalIdReference( + value=cfn_dest_resource_logical_id, + resource_type=self._resource_pair.destination_resource_tf[cfn_dest_resource_logical_id].get( + "type", "" + ), + ) + ) + return dest_resources # it means the source resource is referring to a wrong destination resource type LOG.debug( "The used reference %s is not the correct destination resource type.", resolved_destination_resource.value ) - raise InvalidResourceLinkingException( + expected_destinations_strings = [ + f"destination resource type {expected_destination.terraform_resource_type_prefix} using " + f"{expected_destination.terraform_attribute_name} property" + for expected_destination in self._resource_pair.expected_destinations + ] + raise UnexpectedDestinationResource( f"Could not use the value {resolved_destination_resource.value} as a destination for the source resource " - f"{source_tf_resource.full_address}. The source resource value should refer to valid destination ARN " - f"property." + f"{source_tf_resource.full_address}. The expected destination resources should be of " + f"{', '.join(expected_destinations_strings)}." ) @@ -1011,38 +1045,70 @@ def _link_gateway_resources_to_gateway_rest_apis( Dictionary of all actual terraform Rest API resources (not configuration resources). The dictionary's key is the calculated logical id for each resource. """ - resource_linking_pairs = [ - ResourceLinkingPair( - source_resource_cfn_resource=gateway_resources_cfn_resources, - source_resource_tf_config=gateway_resources_tf_configs, - destination_resource_tf=rest_apis_terraform_resources, - tf_destination_attribute_name="id", - terraform_link_field_name="rest_api_id", - cfn_link_field_name="RestApiId", - terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, - cfn_resource_update_call_back_function=_link_gateway_resource_to_gateway_rest_apis_rest_api_id_call_back, - linking_exceptions=ResourcePairExceptions( - multiple_resource_linking_exception=OneGatewayResourceToRestApiLinkingLimitationException, - local_variable_linking_exception=GatewayResourceToGatewayRestApiLocalVariablesLinkingLimitationException, + resource_linking_pair = ResourceLinkingPair( + source_resource_cfn_resource=gateway_resources_cfn_resources, + source_resource_tf_config=gateway_resources_tf_configs, + destination_resource_tf=rest_apis_terraform_resources, + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", ), + ], + terraform_link_field_name="rest_api_id", + cfn_link_field_name="RestApiId", + cfn_resource_update_call_back_function=_link_gateway_resource_to_gateway_rest_apis_rest_api_id_call_back, + linking_exceptions=ResourcePairExceptions( + multiple_resource_linking_exception=OneGatewayResourceToRestApiLinkingLimitationException, + local_variable_linking_exception=GatewayResourceToGatewayRestApiLocalVariablesLinkingLimitationException, ), - ResourceLinkingPair( - source_resource_cfn_resource=gateway_resources_cfn_resources, - source_resource_tf_config=gateway_resources_tf_configs, - destination_resource_tf=rest_apis_terraform_resources, - tf_destination_attribute_name="root_resource_id", - terraform_link_field_name="parent_id", - cfn_link_field_name="ResourceId", - terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, - cfn_resource_update_call_back_function=_link_gateway_resource_to_gateway_rest_apis_parent_id_call_back, - linking_exceptions=ResourcePairExceptions( - multiple_resource_linking_exception=OneGatewayResourceToRestApiLinkingLimitationException, - local_variable_linking_exception=GatewayResourceToGatewayRestApiLocalVariablesLinkingLimitationException, + ) + ResourceLinker(resource_linking_pair).link_resources() + + +def _link_gateway_resources_to_parents( + gateway_resources_tf_configs: Dict[str, TFResource], + gateway_resources_cfn_resources: Dict[str, List], + multiple_destination_options_terraform_resources: Dict[str, Dict], +): + """ + Iterate through all the resources and link them to the corresponding parent resource. Parent resource can either be + a Rest API resource or another API Resource resource. + + Parameters + ---------- + gateway_resources_tf_configs: Dict[str, TFResource] + Dictionary of configuration Gateway Resource resources + gateway_resources_cfn_resources: Dict[str, List] + Dictionary containing resolved configuration addresses matched up to the cfn Gateway Resource + multiple_destination_options_terraform_resources: Dict[str, Dict] + Dictionary of all actual terraform Rest API resources or Gateway Resources. The dictionary's key is the + calculated logical id for each resource. + """ + + resource_linking_pair = ResourceLinkingPair( + source_resource_cfn_resource=gateway_resources_cfn_resources, + source_resource_tf_config=gateway_resources_tf_configs, + destination_resource_tf=multiple_destination_options_terraform_resources, + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="root_resource_id", + ), + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_RESOURCE_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", ), + ], + terraform_link_field_name="parent_id", + cfn_link_field_name="ParentId", + cfn_resource_update_call_back_function=_link_gateway_resource_to_parent_resource_call_back, + linking_exceptions=ResourcePairExceptions( + multiple_resource_linking_exception=OneGatewayResourceToParentResourceLinkingLimitationException, + local_variable_linking_exception=GatewayResourceToParentResourceLocalVariablesLinkingLimitationException, ), - ] - for resource_linking_pair in resource_linking_pairs: - ResourceLinker(resource_linking_pair).link_resources() + ) + ResourceLinker(resource_linking_pair).link_resources() def _link_lambda_functions_to_layers( @@ -1071,10 +1137,14 @@ def _link_lambda_functions_to_layers( source_resource_cfn_resource=lambda_funcs_conf_cfn_resources, source_resource_tf_config=lambda_config_funcs_conf_cfn_resources, destination_resource_tf=lambda_layers_terraform_resources, - tf_destination_attribute_name="arn", + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=LAMBDA_LAYER_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="arn", + ), + ], terraform_link_field_name="layers", cfn_link_field_name="Layers", - terraform_resource_type_prefix=LAMBDA_LAYER_RESOURCE_ADDRESS_PREFIX, cfn_resource_update_call_back_function=_link_lambda_functions_to_layers_call_back, linking_exceptions=exceptions, ) @@ -1140,36 +1210,40 @@ def _link_gateway_resource_to_gateway_resource_call_back( ) -def _link_gateway_resource_to_gateway_rest_apis_parent_id_call_back( - gateway_cfn_resource: Dict, referenced_rest_apis_values: List[ReferenceType] +def _link_gateway_resource_to_parent_resource_call_back( + gateway_resource_cfn_resource: Dict, referenced_parent_resources_values: List[ReferenceType] ) -> None: """ - Callback function that used by the linking algorithm to update an Api Gateway Resource CFN Resource with - a reference to the Rest Api resource. + Callback function that is used by the linking algorithm to update an Api Gateway resource CFN with a reference to + the parent resource which either rest api or gateway resource. Parameters ---------- - gateway_cfn_resource: Dict - API Gateway Method CFN resource - referenced_rest_apis_values: List[ReferenceType] - List of referenced REST API either as the logical id of REST API resource defined in the customer project, or - ARN values for actual REST API resource defined in customer's account. This list should always contain one - element only. + gateway_resource_cfn_resource: Dict + API Gateway resource CFN resource + referenced_parent_resources_values: List[ReferenceType] + List of referenced parent resources either as the logical id of resource defined in the customer project, + or ARN values for actual parent resource defined in customer's account. This list should always contain + one element only. """ - # if the destination rest api list contains more than one element, so we have an issue in our linking logic - if len(referenced_rest_apis_values) > 1: - raise InvalidResourceLinkingException("Could not link multiple Rest APIs to one Gateway resource") + if len(referenced_parent_resources_values) > 1: + raise InvalidResourceLinkingException("Could not link multiple parent Resources to one Gateway resource") - if not referenced_rest_apis_values: - LOG.info("Unable to find any references to Rest APIs, skip linking Rest API to Gateway resource") + if not referenced_parent_resources_values: + LOG.info("Unable to find any references to the parent resource, skip linking Gateway resources") return - logical_id = referenced_rest_apis_values[0] - gateway_cfn_resource["Properties"]["ParentId"] = ( - {"Fn::GetAtt": [logical_id.value, "RootResourceId"]} - if isinstance(logical_id, LogicalIdReference) - else logical_id.value - ) + logical_id = referenced_parent_resources_values[0] + if isinstance(logical_id, LogicalIdReference): + if logical_id.resource_type == TF_AWS_API_GATEWAY_REST_API: + gateway_resource_cfn_resource["Properties"]["ParentId"] = { + "Fn::GetAtt": [logical_id.value, "RootResourceId"] + } + else: + gateway_resource_cfn_resource["Properties"]["ParentId"] = {"Ref": logical_id.value} + + else: + gateway_resource_cfn_resource["Properties"]["ParentId"] = logical_id.value def _link_gateway_methods_to_gateway_rest_apis( @@ -1199,10 +1273,14 @@ def _link_gateway_methods_to_gateway_rest_apis( source_resource_cfn_resource=gateway_methods_config_address_cfn_resources_map, source_resource_tf_config=gateway_methods_config_resources, destination_resource_tf=rest_apis_terraform_resources, - tf_destination_attribute_name="id", + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], terraform_link_field_name="rest_api_id", cfn_link_field_name="RestApiId", - terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, cfn_resource_update_call_back_function=_link_gateway_resource_to_gateway_rest_apis_rest_api_id_call_back, linking_exceptions=exceptions, ) @@ -1235,10 +1313,14 @@ def _link_gateway_stage_to_rest_api( source_resource_cfn_resource=gateway_stages_config_address_cfn_resources_map, source_resource_tf_config=gateway_stages_config_resources, destination_resource_tf=rest_apis_terraform_resources, - tf_destination_attribute_name="id", + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], terraform_link_field_name="rest_api_id", cfn_link_field_name="RestApiId", - terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, cfn_resource_update_call_back_function=_link_gateway_resource_to_gateway_rest_apis_rest_api_id_call_back, linking_exceptions=exceptions, ) @@ -1272,10 +1354,14 @@ def _link_gateway_method_to_gateway_resource( source_resource_cfn_resource=gateway_method_config_address_cfn_resources_map, source_resource_tf_config=gateway_method_config_resources, destination_resource_tf=gateway_resources_terraform_resources, - tf_destination_attribute_name="id", + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_RESOURCE_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], terraform_link_field_name="resource_id", cfn_link_field_name="ResourceId", - terraform_resource_type_prefix=API_GATEWAY_RESOURCE_RESOURCE_ADDRESS_PREFIX, cfn_resource_update_call_back_function=_link_gateway_resource_to_gateway_resource_call_back, linking_exceptions=exceptions, ) @@ -1309,10 +1395,14 @@ def _link_gateway_integrations_to_gateway_rest_apis( source_resource_cfn_resource=gateway_integrations_config_address_cfn_resources_map, source_resource_tf_config=gateway_integrations_config_resources, destination_resource_tf=rest_apis_terraform_resources, - tf_destination_attribute_name="id", + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], terraform_link_field_name="rest_api_id", cfn_link_field_name="RestApiId", - terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, cfn_resource_update_call_back_function=_link_gateway_resource_to_gateway_rest_apis_rest_api_id_call_back, linking_exceptions=exceptions, ) @@ -1347,10 +1437,14 @@ def _link_gateway_integrations_to_gateway_resource( source_resource_cfn_resource=gateway_integrations_config_address_cfn_resources_map, source_resource_tf_config=gateway_integrations_config_resources, destination_resource_tf=gateway_resources_terraform_resources, - tf_destination_attribute_name="id", + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_RESOURCE_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], terraform_link_field_name="resource_id", cfn_link_field_name="ResourceId", - terraform_resource_type_prefix=API_GATEWAY_RESOURCE_RESOURCE_ADDRESS_PREFIX, cfn_resource_update_call_back_function=_link_gateway_resource_to_gateway_resource_call_back, linking_exceptions=exceptions, ) @@ -1425,10 +1519,14 @@ def _link_gateway_integrations_to_function_resource( source_resource_cfn_resource=gateway_integrations_config_address_cfn_resources_map, source_resource_tf_config=aws_proxy_integrations_config_resources, destination_resource_tf=lambda_function_terraform_resources, - tf_destination_attribute_name="invoke_arn", + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=LAMBDA_FUNCTION_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="invoke_arn", + ), + ], terraform_link_field_name="uri", cfn_link_field_name="Uri", - terraform_resource_type_prefix=LAMBDA_FUNCTION_RESOURCE_ADDRESS_PREFIX, cfn_resource_update_call_back_function=_link_gateway_integration_to_function_call_back, linking_exceptions=exceptions, ) @@ -1464,10 +1562,14 @@ def _link_gateway_integration_responses_to_gateway_rest_apis( source_resource_cfn_resource=gateway_integration_responses_config_address_cfn_resources_map, source_resource_tf_config=gateway_integration_responses_config_resources, destination_resource_tf=rest_apis_terraform_resources, - tf_destination_attribute_name="id", + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], terraform_link_field_name="rest_api_id", cfn_link_field_name="RestApiId", - terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, cfn_resource_update_call_back_function=_link_gateway_resource_to_gateway_rest_apis_rest_api_id_call_back, linking_exceptions=exceptions, ) @@ -1502,10 +1604,14 @@ def _link_gateway_integration_responses_to_gateway_resource( source_resource_cfn_resource=gateway_integration_responses_config_address_cfn_resources_map, source_resource_tf_config=gateway_integration_responses_config_resources, destination_resource_tf=gateway_resources_terraform_resources, - tf_destination_attribute_name="id", + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_RESOURCE_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], terraform_link_field_name="resource_id", cfn_link_field_name="ResourceId", - terraform_resource_type_prefix=API_GATEWAY_RESOURCE_RESOURCE_ADDRESS_PREFIX, cfn_resource_update_call_back_function=_link_gateway_resource_to_gateway_resource_call_back, linking_exceptions=exceptions, ) @@ -1571,10 +1677,14 @@ def _link_gateway_authorizer_to_lambda_function( source_resource_cfn_resource=authorizer_cfn_resources, source_resource_tf_config=authorizer_config_resources, destination_resource_tf=lamda_function_resources, - tf_destination_attribute_name="invoke_arn", + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=LAMBDA_FUNCTION_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="invoke_arn", + ), + ], terraform_link_field_name="authorizer_uri", cfn_link_field_name="AuthorizerUri", - terraform_resource_type_prefix=LAMBDA_FUNCTION_RESOURCE_ADDRESS_PREFIX, cfn_resource_update_call_back_function=_link_gateway_authorizer_to_lambda_function_call_back, linking_exceptions=exceptions, ) @@ -1607,10 +1717,14 @@ def _link_gateway_authorizer_to_rest_api( source_resource_cfn_resource=authorizer_cfn_resources, source_resource_tf_config=authorizer_config_resources, destination_resource_tf=rest_api_resource, - tf_destination_attribute_name="id", + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], terraform_link_field_name="rest_api_id", cfn_link_field_name="RestApiId", - terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, cfn_resource_update_call_back_function=_link_gateway_resource_to_gateway_rest_apis_rest_api_id_call_back, linking_exceptions=exceptions, ) @@ -1673,10 +1787,14 @@ def _link_gateway_method_to_gateway_authorizer( source_resource_cfn_resource=gateway_method_config_address_cfn_resources_map, source_resource_tf_config=gateway_method_config_resources, destination_resource_tf=authorizer_resources, - tf_destination_attribute_name="id", + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_AUTHORIZER_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], terraform_link_field_name="authorizer_id", cfn_link_field_name="AuthorizerId", - terraform_resource_type_prefix=API_GATEWAY_AUTHORIZER_RESOURCE_ADDRESS_PREFIX, cfn_resource_update_call_back_function=_link_gateway_method_to_gateway_authorizer_call_back, linking_exceptions=exceptions, ) 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 b91478580a..dc5f3080f5 100644 --- a/samcli/hook_packages/terraform/hooks/prepare/resources/resource_links.py +++ b/samcli/hook_packages/terraform/hooks/prepare/resources/resource_links.py @@ -1,6 +1,6 @@ from typing import List -from samcli.hook_packages.terraform.hooks.prepare.property_builder import ( +from samcli.hook_packages.terraform.hooks.prepare.constants import ( TF_AWS_API_GATEWAY_AUTHORIZER, TF_AWS_API_GATEWAY_INTEGRATION, TF_AWS_API_GATEWAY_INTEGRATION_RESPONSE, @@ -23,10 +23,14 @@ _link_gateway_method_to_gateway_resource, _link_gateway_methods_to_gateway_rest_apis, _link_gateway_resources_to_gateway_rest_apis, + _link_gateway_resources_to_parents, _link_gateway_stage_to_rest_api, _link_lambda_functions_to_layers, ) -from samcli.hook_packages.terraform.hooks.prepare.types import LinkingPairCaller +from samcli.hook_packages.terraform.hooks.prepare.types import ( + LinkingMultipleDestinationsOptionsCaller, + LinkingPairCaller, +) RESOURCE_LINKS: List[LinkingPairCaller] = [ LinkingPairCaller( @@ -91,3 +95,11 @@ linking_func=_link_gateway_method_to_gateway_authorizer, ), ] + +MULTIPLE_DESTINATIONS_RESOURCE_LINKS: List[LinkingMultipleDestinationsOptionsCaller] = [ + LinkingMultipleDestinationsOptionsCaller( + source=TF_AWS_API_GATEWAY_RESOURCE, + destinations=[TF_AWS_API_GATEWAY_REST_API, TF_AWS_API_GATEWAY_RESOURCE], + linking_func=_link_gateway_resources_to_parents, + ), +] diff --git a/samcli/hook_packages/terraform/hooks/prepare/resources/resource_properties.py b/samcli/hook_packages/terraform/hooks/prepare/resources/resource_properties.py index 30c31aabd0..f190eb43d2 100644 --- a/samcli/hook_packages/terraform/hooks/prepare/resources/resource_properties.py +++ b/samcli/hook_packages/terraform/hooks/prepare/resources/resource_properties.py @@ -1,7 +1,7 @@ """Module for getting the resource property mappings for various resource types""" from typing import Dict -from samcli.hook_packages.terraform.hooks.prepare.property_builder import ( +from samcli.hook_packages.terraform.hooks.prepare.constants import ( TF_AWS_API_GATEWAY_AUTHORIZER, TF_AWS_API_GATEWAY_INTEGRATION, TF_AWS_API_GATEWAY_INTEGRATION_RESPONSE, diff --git a/samcli/hook_packages/terraform/hooks/prepare/translate.py b/samcli/hook_packages/terraform/hooks/prepare/translate.py index 14f9e73733..46dcb15c97 100644 --- a/samcli/hook_packages/terraform/hooks/prepare/translate.py +++ b/samcli/hook_packages/terraform/hooks/prepare/translate.py @@ -9,16 +9,16 @@ from samcli.hook_packages.terraform.hooks.prepare.constants import ( CFN_CODE_PROPERTIES, - SAM_METADATA_RESOURCE_NAME_ATTRIBUTE, -) -from samcli.hook_packages.terraform.hooks.prepare.enrich import enrich_resources_and_generate_makefile -from samcli.hook_packages.terraform.hooks.prepare.property_builder import ( REMOTE_DUMMY_VALUE, - RESOURCE_TRANSLATOR_MAPPING, + SAM_METADATA_RESOURCE_NAME_ATTRIBUTE, TF_AWS_API_GATEWAY_INTEGRATION, TF_AWS_API_GATEWAY_INTEGRATION_RESPONSE, TF_AWS_API_GATEWAY_METHOD, TF_AWS_API_GATEWAY_REST_API, +) +from samcli.hook_packages.terraform.hooks.prepare.enrich import enrich_resources_and_generate_makefile +from samcli.hook_packages.terraform.hooks.prepare.property_builder import ( + RESOURCE_TRANSLATOR_MAPPING, PropertyBuilderMapping, ) from samcli.hook_packages.terraform.hooks.prepare.resource_linking import ( @@ -31,7 +31,10 @@ add_integrations_to_methods, ) from samcli.hook_packages.terraform.hooks.prepare.resources.internal import INTERNAL_PREFIX -from samcli.hook_packages.terraform.hooks.prepare.resources.resource_links import RESOURCE_LINKS +from samcli.hook_packages.terraform.hooks.prepare.resources.resource_links import ( + MULTIPLE_DESTINATIONS_RESOURCE_LINKS, + RESOURCE_LINKS, +) from samcli.hook_packages.terraform.hooks.prepare.resources.resource_properties import get_resource_property_mapping from samcli.hook_packages.terraform.hooks.prepare.types import ( CodeResourceProperties, @@ -339,11 +342,25 @@ def translate_to_cfn(tf_json: dict, output_directory_path: str, terraform_applic def _handle_linking(resource_property_mapping: Dict[str, ResourceProperties]) -> None: - for links in RESOURCE_LINKS: - links.linking_func( - resource_property_mapping[links.source].terraform_config, - resource_property_mapping[links.source].cfn_resources, - resource_property_mapping[links.dest].terraform_resources, + for link in RESOURCE_LINKS: + link.linking_func( + resource_property_mapping[link.source].terraform_config, + resource_property_mapping[link.source].cfn_resources, + resource_property_mapping[link.dest].terraform_resources, + ) + + for multiple_destinations_link in MULTIPLE_DESTINATIONS_RESOURCE_LINKS: + destinations: Dict[str, Dict] = {} + for dest_resource_type in multiple_destinations_link.destinations: + destinations = { + **destinations, + **resource_property_mapping[dest_resource_type].terraform_resources, + } + + multiple_destinations_link.linking_func( + resource_property_mapping[multiple_destinations_link.source].terraform_config, + resource_property_mapping[multiple_destinations_link.source].cfn_resources, + destinations, ) diff --git a/samcli/hook_packages/terraform/hooks/prepare/types.py b/samcli/hook_packages/terraform/hooks/prepare/types.py index d88c8b9103..361ddc95fd 100644 --- a/samcli/hook_packages/terraform/hooks/prepare/types.py +++ b/samcli/hook_packages/terraform/hooks/prepare/types.py @@ -88,6 +88,12 @@ class LinkingPairCaller(NamedTuple): linking_func: Callable[[Dict[str, TFResource], Dict[str, List], Dict[str, Dict]], None] +class LinkingMultipleDestinationsOptionsCaller(NamedTuple): + source: str + destinations: List[str] + linking_func: Callable[[Dict[str, TFResource], Dict[str, List], Dict[str, Dict[str, Dict]]], None] + + class ResourceTranslationValidator: """ Base class for a validation class to be used when translating Terraform resources to a metadata file diff --git a/tests/integration/local/start_api/test_start_api_with_terraform_application.py b/tests/integration/local/start_api/test_start_api_with_terraform_application.py index e8ab21d01d..8953ec780f 100644 --- a/tests/integration/local/start_api/test_start_api_with_terraform_application.py +++ b/tests/integration/local/start_api/test_start_api_with_terraform_application.py @@ -94,18 +94,29 @@ def tearDownClass(cls) -> None: not CI_OVERRIDE, "Skip Terraform test cases unless running in CI", ) +@parameterized_class( + [ + { + "terraform_application": "terraform-v1-nested-apis", + "testing_urls": ["parent/hello", "parent"], + }, + { + "terraform_application": "terraform-v1-api-simple", + "testing_urls": ["hello"], + }, + ] +) @pytest.mark.flaky(reruns=3) class TestStartApiTerraformApplication(TerraformStartApiIntegrationBase): - terraform_application = "terraform-v1-api-simple" - def setUp(self): self.url = "http://127.0.0.1:{}".format(self.port) def test_successful_request(self): - response = requests.get(self.url + "/hello", timeout=300) + for url in self.testing_urls: + response = requests.get(f"{self.url}/{url}", timeout=300) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), {"message": "hello world"}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"message": "hello world"}) @skipIf( @@ -172,11 +183,21 @@ def test_unsupported_limitations(self): @pytest.mark.flaky(reruns=3) @parameterized_class( [ + { + "terraform_application": "terraform-v1-nested-apis", + "testing_urls": ["parent/hello", "parent"], + }, + { + "terraform_application": "terraform-v1-api-simple", + "testing_urls": ["hello"], + }, { "terraform_application": "terraform-api-simple-multiple-resources-limitation", + "testing_urls": ["hello"], }, { "terraform_application": "terraform-api-simple-local-variables-limitation", + "testing_urls": ["hello"], }, ] ) @@ -185,10 +206,11 @@ def setUp(self): self.url = "http://127.0.0.1:{}".format(self.port) def test_successful_request(self): - response = requests.get(self.url + "/hello", timeout=300) + for url in self.testing_urls: + response = requests.get(f"{self.url}/{url}", timeout=300) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), {"message": "hello world"}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"message": "hello world"}) @skipIf( diff --git a/tests/integration/testdata/start_api/terraform/terraform-v1-api-simple/main.tf b/tests/integration/testdata/start_api/terraform/terraform-v1-api-simple/main.tf index 7f64aca6ad..43012c539e 100644 --- a/tests/integration/testdata/start_api/terraform/terraform-v1-api-simple/main.tf +++ b/tests/integration/testdata/start_api/terraform/terraform-v1-api-simple/main.tf @@ -1,8 +1,14 @@ provider "aws" { } +resource "random_uuid" "unique_id" { + keepers = { + my_key = "my_key" + } +} + resource "aws_iam_role" "iam_for_lambda" { - name = "iam_for_lambda" + name = "iam_for_lambda_${random_uuid.unique_id.result}" assume_role_policy = <d% zj=rr_mB#73$cT&^d&Rnvd~@F3nr0UsKKwd(1On*(k|vmU$lC-Lv;C$Aq$o-7pq&E{M-q~^XxoMvJEv_l+m@7jTa9;hPw$6Vy`wtz-{!cZE_a|D; zeVDjm-otwV`j)dFmQUoFvQj%^=GxV#jB;WW`yak~>TfKv^`&=a>12=7dUs@}Yn^z} zFu?8)zcdJTqJ=t z?SE6fiLK;bUk3&I4etb<+SUg09kOvSntbo~f?C7+ga0qrO)`tl_p0VQ<7}sSdh&*g z$w~E2-%Miq^?$!E*jEv^Y;xM}6Fw>bvpuG-H<`PiX<4zU?Xji0HFNg=UgQ|py+YGt zRrH$pt#d_Oa|5T`@Htzt>Z0C$pN~`I-W@BlkX^#O$1aogyad~GpBYa?Oit|OG%Q@r zJne+QhMg=wGo;F|=4x6*KUUklYT^|>#^BDNRr{aKXiYwO@2*tqq9%TG^V?6a-djyoq`xwJ&A=1r>d(6HZAx$U$5MgY z6aJ*VtGvDB-}X1=;G`9O{2n_nZGjRH_M`<$cLKnqRg_v-npu>Zo0?ZrtXEP|LQc|U zWD;RU%@?p_4ayfVu%r>hA|aOqc%vDN None: source_resource_cfn_resource=Mock(), source_resource_tf_config=Mock(), destination_resource_tf={ - "applied_layer1_logical_id": {"values": {"arn": "applied_layer1.arn", "filename": "/some/path"}}, - "applied_layer2_logical_id": {"values": {"arn": "applied_layer2.arn", "filename": "/some/path"}}, - "noo_applied_layer3_logical_id": {"values": {"filename": "/some/path"}}, + "applied_layer1_logical_id": { + "values": {"arn": "applied_layer1.arn", "filename": "/some/path"}, + "type": TF_AWS_LAMBDA_LAYER_VERSION, + }, + "applied_layer2_logical_id": { + "values": {"arn": "applied_layer2.arn", "filename": "/some/path"}, + "type": TF_AWS_LAMBDA_LAYER_VERSION, + }, + "noo_applied_layer3_logical_id": { + "values": {"filename": "/some/path"}, + "type": TF_AWS_LAMBDA_LAYER_VERSION, + }, }, - tf_destination_attribute_name="arn", + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=LAMBDA_LAYER_RESOURCE_ADDRESS_PREFIX, terraform_attribute_name="arn" + ) + ], terraform_link_field_name="layers", cfn_link_field_name="Layers", - terraform_resource_type_prefix=LAMBDA_LAYER_RESOURCE_ADDRESS_PREFIX, cfn_resource_update_call_back_function=Mock(), linking_exceptions=self.linker_exceptions, ) @@ -1295,7 +1319,7 @@ def test_link_using_linking_fields_mix_existing_and_applied_resources(self): resource_linker = ResourceLinker(self.sample_resource_linking_pair) resource_linker._link_using_linking_fields(cfn_resource) dest_resources = [ - LogicalIdReference("applied_layer1_logical_id"), + LogicalIdReference(value="applied_layer1_logical_id", resource_type=TF_AWS_LAMBDA_LAYER_VERSION), ExistingResourceReference("existing_layer1.arn"), ] self.sample_resource_linking_pair.cfn_resource_update_call_back_function.assert_called_with( @@ -1412,11 +1436,16 @@ def test_process_reference_resource_value_reference_to_an_existing_layer_resourc reference_resolved_layer = ResolvedReference("aws_lambda_layer_version.layer.arn", "module.layer1") resource = Mock() resource_linker = ResourceLinker(self.sample_resource_linking_pair) - resource_linker._resource_pair.destination_resource_tf = {"layer1LogicalId": Mock()} + + resource_linker._resource_pair.destination_resource_tf = { + "layer1LogicalId": {"values": Mock(), "type": TF_AWS_LAMBDA_LAYER_VERSION} + } resources = resource_linker._process_reference_resource_value(resource, reference_resolved_layer) self.assertEqual(len(resources), 1) - self.assertEqual(resources[0], LogicalIdReference("layer1LogicalId")) + self.assertEqual( + resources[0], LogicalIdReference(value="layer1LogicalId", resource_type=TF_AWS_LAMBDA_LAYER_VERSION) + ) build_cfn_logical_id_mock.assert_called_with("module.layer1.aws_lambda_layer_version.layer") @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.build_cfn_logical_id") @@ -1436,10 +1465,10 @@ def test_process_reference_layer_value_reference_to_not_layer_resource_arn_prope resource = Mock() resource.full_address = "func_full_address" expected_exception = ( - f"An error occurred when attempting to link two resources: Could not use the value " - f"aws_lambda_layer_version.layer.name as a destination resource for the source " - f"resource func_full_address. The source resource value should refer to valid destination " - f"resource ARN property." + "An error occurred when attempting to link two resources: Could not use the value " + "aws_lambda_layer_version.layer.name as a destination for the source resource func_full_address. " + "The expected destination resources should be of destination resource type aws_lambda_layer_version. " + "using arn property." ) resource_linker = ResourceLinker(self.sample_resource_linking_pair) with self.assertRaises(InvalidResourceLinkingException) as exc: @@ -1451,9 +1480,10 @@ def test_process_reference_resource_value_reference_to_invalid_destination_resou resource = Mock() resource.full_address = "func_full_address" expected_exception = ( - f"An error occurred when attempting to link two resources: Could not use the value " - f"aws_lambda_layer_version2.layer.arn as a destination for the source resource func_full_address. " - f"The source resource value should refer to valid destination ARN property." + "An error occurred when attempting to link two resources: Could not use the value " + "aws_lambda_layer_version2.layer.arn as a destination for the source resource func_full_address. " + "The expected destination resources should be of destination resource type aws_lambda_layer_version. " + "using arn property." ) resource_linker = ResourceLinker(self.sample_resource_linking_pair) with self.assertRaises(InvalidResourceLinkingException) as exc: @@ -1596,10 +1626,14 @@ def test_link_lambda_functions_to_layers( source_resource_cfn_resource=lambda_funcs_config_resources, source_resource_tf_config=resources, destination_resource_tf=terraform_layers_resources, - tf_destination_attribute_name="arn", + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=LAMBDA_LAYER_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="arn", + ), + ], terraform_link_field_name="layers", cfn_link_field_name="Layers", - terraform_resource_type_prefix=LAMBDA_LAYER_RESOURCE_ADDRESS_PREFIX, cfn_resource_update_call_back_function=mock_link_lambda_functions_to_layers_call_back, linking_exceptions=mock_resource_linking_exceptions(), ) @@ -1613,7 +1647,10 @@ def test_link_lambda_functions_to_layers( "Properties": {"FunctionName": "func", "Layers": ["existing_layer1.arn", "applied_layer1.arn"]}, "Metadata": {"SamResourceId": "aws_lambda_function.remote_lambda_code", "SkipBuild": True}, }, - [ExistingResourceReference("existing_layer1.arn"), LogicalIdReference("Layer1LogicaId")], + [ + ExistingResourceReference("existing_layer1.arn"), + LogicalIdReference(value="Layer1LogicaId", resource_type=TF_AWS_LAMBDA_LAYER_VERSION), + ], ["existing_layer1.arn", {"Ref": "Layer1LogicaId"}], ), ( @@ -1633,7 +1670,7 @@ def test_link_lambda_functions_to_layers( }, "Metadata": {"SamResourceId": "aws_lambda_function.remote_lambda_code", "SkipBuild": True}, }, - [LogicalIdReference("Layer1LogicaId")], + [LogicalIdReference(value="Layer1LogicaId", resource_type=TF_AWS_LAMBDA_LAYER_VERSION)], [{"Ref": "Layer1LogicaId"}], ), ] @@ -1671,20 +1708,65 @@ def test_link_gateway_methods_to_gateway_rest_apis( source_resource_cfn_resource=gateway_method_config_resources, source_resource_tf_config=resources, destination_resource_tf=terraform_rest_apis_resources, - tf_destination_attribute_name="id", + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], terraform_link_field_name="rest_api_id", cfn_link_field_name="RestApiId", - terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, cfn_resource_update_call_back_function=mock_link_gateway_methods_to_gateway_rest_apis_call_back, linking_exceptions=mock_resource_linking_exceptions(), ) mock_resource_linker.assert_called_once_with(mock_resource_linking_pair()) @patch( - "samcli.hook_packages.terraform.hooks.prepare.resource_linking._link_gateway_resource_to_gateway_rest_apis_rest_api_id_call_back" + "samcli.hook_packages.terraform.hooks.prepare.resource_linking._link_gateway_resource_to_parent_resource_call_back" ) + @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_resources_to_parents( + self, + mock_resource_linking_exceptions, + mock_resource_linking_pair, + mock_resource_linker, + mock_link_gateway_resource_to_parent_resource_call_back, + ): + gateway_resource_config_resources = Mock() + terraform_parent_resources = Mock() + resources = Mock() + _link_gateway_resources_to_parents(resources, gateway_resource_config_resources, terraform_parent_resources) + + mock_resource_linking_exceptions.assert_called_once_with( + multiple_resource_linking_exception=OneGatewayResourceToParentResourceLinkingLimitationException, + local_variable_linking_exception=GatewayResourceToParentResourceLocalVariablesLinkingLimitationException, + ) + + mock_resource_linking_pair.assert_called_once_with( + source_resource_cfn_resource=gateway_resource_config_resources, + source_resource_tf_config=resources, + destination_resource_tf=terraform_parent_resources, + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="root_resource_id", + ), + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_RESOURCE_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], + terraform_link_field_name="parent_id", + cfn_link_field_name="ParentId", + cfn_resource_update_call_back_function=mock_link_gateway_resource_to_parent_resource_call_back, + linking_exceptions=mock_resource_linking_exceptions(), + ) + mock_resource_linker.assert_called_once_with(mock_resource_linking_pair()) + @patch( - "samcli.hook_packages.terraform.hooks.prepare.resource_linking._link_gateway_resource_to_gateway_rest_apis_parent_id_call_back" + "samcli.hook_packages.terraform.hooks.prepare.resource_linking._link_gateway_resource_to_gateway_rest_apis_rest_api_id_call_back" ) @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourceLinker") @patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking.ResourceLinkingPair") @@ -1694,7 +1776,6 @@ def test_link_gateway_resources_to_gateway_rest_apis( mock_resource_linking_exceptions, mock_resource_linking_pair, mock_resource_linker, - mock_link_gateway_methods_to_gateway_rest_apis_parent_id_call_back, mock_link_gateway_methods_to_gateway_rest_apis_rest_api_id_call_back, ): gateway_resource_config_resources = Mock() @@ -1703,53 +1784,28 @@ def test_link_gateway_resources_to_gateway_rest_apis( _link_gateway_resources_to_gateway_rest_apis( resources, gateway_resource_config_resources, terraform_rest_apis_resources ) - mock_resource_linking_exceptions.assert_has_calls( - [ - call( - multiple_resource_linking_exception=OneGatewayResourceToRestApiLinkingLimitationException, - local_variable_linking_exception=GatewayResourceToGatewayRestApiLocalVariablesLinkingLimitationException, - ), - call( - multiple_resource_linking_exception=OneGatewayResourceToRestApiLinkingLimitationException, - local_variable_linking_exception=GatewayResourceToGatewayRestApiLocalVariablesLinkingLimitationException, - ), - ] + + mock_resource_linking_exceptions.assert_called_once_with( + multiple_resource_linking_exception=OneGatewayResourceToRestApiLinkingLimitationException, + local_variable_linking_exception=GatewayResourceToGatewayRestApiLocalVariablesLinkingLimitationException, ) - mock_resource_linking_pair.assert_has_calls( - [ - call( - source_resource_cfn_resource=gateway_resource_config_resources, - source_resource_tf_config=resources, - destination_resource_tf=terraform_rest_apis_resources, - tf_destination_attribute_name="id", - terraform_link_field_name="rest_api_id", - cfn_link_field_name="RestApiId", - terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, - cfn_resource_update_call_back_function=mock_link_gateway_methods_to_gateway_rest_apis_rest_api_id_call_back, - linking_exceptions=mock_resource_linking_exceptions(), - ), - call( - source_resource_cfn_resource=gateway_resource_config_resources, - source_resource_tf_config=resources, - destination_resource_tf=terraform_rest_apis_resources, - tf_destination_attribute_name="root_resource_id", - terraform_link_field_name="parent_id", - cfn_link_field_name="ResourceId", + mock_resource_linking_pair.assert_called_once_with( + source_resource_cfn_resource=gateway_resource_config_resources, + source_resource_tf_config=resources, + destination_resource_tf=terraform_rest_apis_resources, + expected_destinations=[ + ResourcePairExceptedDestination( terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, - cfn_resource_update_call_back_function=mock_link_gateway_methods_to_gateway_rest_apis_parent_id_call_back, - linking_exceptions=mock_resource_linking_exceptions(), + terraform_attribute_name="id", ), - ] - ) - mock_resource_linker.assert_has_calls( - [ - call(mock_resource_linking_pair()), - call().link_resources(), - call(mock_resource_linking_pair()), - call().link_resources(), - ] + ], + terraform_link_field_name="rest_api_id", + cfn_link_field_name="RestApiId", + cfn_resource_update_call_back_function=mock_link_gateway_methods_to_gateway_rest_apis_rest_api_id_call_back, + linking_exceptions=mock_resource_linking_exceptions(), ) + mock_resource_linker.assert_called_once_with(mock_resource_linking_pair()) @patch( "samcli.hook_packages.terraform.hooks.prepare.resource_linking._link_gateway_resource_to_gateway_rest_apis_rest_api_id_call_back" @@ -1776,10 +1832,14 @@ def test_link_gateway_stage_to_gateway_rest_apis( source_resource_cfn_resource=gateway_stage_config_resources, source_resource_tf_config=resources, destination_resource_tf=terraform_rest_apis_resources, - tf_destination_attribute_name="id", + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], terraform_link_field_name="rest_api_id", cfn_link_field_name="RestApiId", - terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, cfn_resource_update_call_back_function=mock_link_gateway_resource_to_gateway_rest_apis_call_back, linking_exceptions=mock_resource_linking_exceptions(), ) @@ -1812,10 +1872,14 @@ def test_link_gateway_methods_to_gateway_resources( source_resource_cfn_resource=gateway_method_config_resources, source_resource_tf_config=resources, destination_resource_tf=terraform_resources_resources, - tf_destination_attribute_name="id", + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_RESOURCE_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], terraform_link_field_name="resource_id", cfn_link_field_name="ResourceId", - terraform_resource_type_prefix=API_GATEWAY_RESOURCE_RESOURCE_ADDRESS_PREFIX, cfn_resource_update_call_back_function=mock_link_gateway_method_to_gateway_resource_call_back, linking_exceptions=mock_resource_linking_exceptions(), ) @@ -1848,10 +1912,14 @@ def test_link_gateway_integrations_to_gateway_rest_apis( source_resource_cfn_resource=gateway_integrations_config_resources, source_resource_tf_config=resources, destination_resource_tf=terraform_resources_resources, - tf_destination_attribute_name="id", + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], terraform_link_field_name="rest_api_id", cfn_link_field_name="RestApiId", - terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, cfn_resource_update_call_back_function=mock_link_gateway_integrations_to_gateway_rest_apis, linking_exceptions=mock_resource_linking_exceptions(), ) @@ -1884,10 +1952,14 @@ def test_link_gateway_integrations_to_gateway_resource( source_resource_cfn_resource=gateway_integrations_config_resources, source_resource_tf_config=resources, destination_resource_tf=terraform_resources_resources, - tf_destination_attribute_name="id", + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_RESOURCE_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], terraform_link_field_name="resource_id", cfn_link_field_name="ResourceId", - terraform_resource_type_prefix=API_GATEWAY_RESOURCE_RESOURCE_ADDRESS_PREFIX, cfn_resource_update_call_back_function=mock_link_gateway_resource_to_gateway_resource_call_back, linking_exceptions=mock_resource_linking_exceptions(), ) @@ -1925,10 +1997,14 @@ def test_link_gateway_integrations_to_function_resource( source_resource_cfn_resource=gateway_integrations_config_resources, source_resource_tf_config=expected_aws_proxy_integrations, destination_resource_tf=terraform_resources_resources, - tf_destination_attribute_name="invoke_arn", + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=LAMBDA_FUNCTION_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="invoke_arn", + ), + ], terraform_link_field_name="uri", cfn_link_field_name="Uri", - terraform_resource_type_prefix=LAMBDA_FUNCTION_RESOURCE_ADDRESS_PREFIX, cfn_resource_update_call_back_function=mock_link_gateway_integration_to_function_call_back, linking_exceptions=mock_resource_linking_exceptions(), ) @@ -1941,7 +2017,7 @@ def test_link_gateway_integrations_to_function_resource( "Type": "AWS::ApiGateway::Method", "Properties": {"HttpMethod": "post", "RestApiId": "restapi.id"}, }, - [LogicalIdReference("RestApi1")], + [LogicalIdReference(value="RestApi1", resource_type=TF_AWS_API_GATEWAY_REST_API)], {"Ref": "RestApi1"}, ), ( @@ -1957,7 +2033,7 @@ def test_link_gateway_integrations_to_function_resource( "Type": "AWS::ApiGateway::Method", "Properties": {"HttpMethod": "post"}, }, - [LogicalIdReference("RestApi1")], + [LogicalIdReference(value="RestApi1", resource_type=TF_AWS_API_GATEWAY_REST_API)], {"Ref": "RestApi1"}, ), ] @@ -1977,7 +2053,7 @@ def test_link_gateway_methods_to_gateway_rest_apis_call_back( "Type": "AWS::ApiGateway::Method", "Properties": {"HttpMethod": "post", "ResourceId": "resource.id"}, }, - [LogicalIdReference("Resource1")], + [LogicalIdReference(value="Resource1", resource_type=TF_AWS_API_GATEWAY_RESOURCE)], {"Ref": "Resource1"}, ), ( @@ -1993,7 +2069,7 @@ def test_link_gateway_methods_to_gateway_rest_apis_call_back( "Type": "AWS::ApiGateway::Method", "Properties": {"HttpMethod": "post"}, }, - [LogicalIdReference("Resource1")], + [LogicalIdReference(value="Resource1", resource_type=TF_AWS_API_GATEWAY_RESOURCE)], {"Ref": "Resource1"}, ), ] @@ -2013,7 +2089,7 @@ def test_link_gateway_method_to_gateway_resource_call_back( "Type": "AWS::ApiGateway::Resource", "Properties": {"ParentId": "restapi.parent_id"}, }, - [LogicalIdReference("RestApi")], + [LogicalIdReference(value="RestApi", resource_type=TF_AWS_API_GATEWAY_REST_API)], {"Fn::GetAtt": ["RestApi", "RootResourceId"]}, ), ( @@ -2029,7 +2105,7 @@ def test_link_gateway_method_to_gateway_resource_call_back( "Type": "AWS::ApiGateway::Resource", "Properties": {}, }, - [LogicalIdReference("RestApi")], + [LogicalIdReference(value="RestApi", resource_type=TF_AWS_API_GATEWAY_REST_API)], {"Fn::GetAtt": ["RestApi", "RootResourceId"]}, ), ] @@ -2038,7 +2114,7 @@ def test_link_gateway_resource_to_gateway_rest_api_parent_id_call_back( self, input_gateway_resource, logical_ids, expected_rest_api ): gateway_resource = deepcopy(input_gateway_resource) - _link_gateway_resource_to_gateway_rest_apis_parent_id_call_back(gateway_resource, logical_ids) + _link_gateway_resource_to_parent_resource_call_back(gateway_resource, logical_ids) input_gateway_resource["Properties"]["ParentId"] = expected_rest_api self.assertEqual(gateway_resource, input_gateway_resource) @@ -2049,7 +2125,7 @@ def test_link_gateway_resource_to_gateway_rest_api_parent_id_call_back( "Type": "Internal::ApiGateway::Method::Integration", "Properties": {"Uri": "invoke_arn"}, }, - [LogicalIdReference("FunctionA")], + [LogicalIdReference(value="FunctionA", resource_type=TF_AWS_LAMBDA_FUNCTION)], { "Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${FunctionA.Arn}/invocations" }, @@ -2067,7 +2143,7 @@ def test_link_gateway_resource_to_gateway_rest_api_parent_id_call_back( "Type": "Internal::ApiGateway::Method::Integration", "Properties": {"Uri": "invoke_arn"}, }, - [LogicalIdReference("RestApi")], + [LogicalIdReference(value="RestApi", resource_type=TF_AWS_API_GATEWAY_REST_API)], { "Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${RestApi.Arn}/invocations" }, @@ -2093,8 +2169,8 @@ def test_link_gateway_integration_to_function_call_back( "Could not link multiple Lambda functions to one Gateway Authorizer", ), ( - _link_gateway_resource_to_gateway_rest_apis_parent_id_call_back, - "Could not link multiple Rest APIs to one Gateway resource", + _link_gateway_resource_to_parent_resource_call_back, + "Could not link multiple parent Resources to one Gateway resource", ), ( _link_gateway_resource_to_gateway_resource_call_back, @@ -2118,7 +2194,7 @@ def test_linking_callbacks_raises_multiple_reference_exception(self, linking_cal [ (_link_gateway_integration_to_function_call_back,), (_link_gateway_authorizer_to_lambda_function_call_back,), - (_link_gateway_resource_to_gateway_rest_apis_parent_id_call_back,), + (_link_gateway_resource_to_parent_resource_call_back,), (_link_gateway_resource_to_gateway_resource_call_back,), (_link_gateway_resource_to_gateway_rest_apis_rest_api_id_call_back,), (_link_gateway_method_to_gateway_authorizer_call_back,), @@ -2159,10 +2235,14 @@ def test_link_gateway_integration_responses_to_gateway_rest_apis( source_resource_cfn_resource=gateway_integration_responses_config_resources, source_resource_tf_config=resources, destination_resource_tf=terraform_resources_resources, - tf_destination_attribute_name="id", + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], terraform_link_field_name="rest_api_id", cfn_link_field_name="RestApiId", - terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, cfn_resource_update_call_back_function=mock_link_gateway_integrations_to_gateway_rest_apis, linking_exceptions=mock_resource_linking_exceptions(), ) @@ -2195,10 +2275,14 @@ def test_link_gateway_integration_response_to_gateway_resource( source_resource_cfn_resource=gateway_integration_responses_config_resources, source_resource_tf_config=resources, destination_resource_tf=terraform_resources_resources, - tf_destination_attribute_name="id", + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_RESOURCE_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], terraform_link_field_name="resource_id", cfn_link_field_name="ResourceId", - terraform_resource_type_prefix=API_GATEWAY_RESOURCE_RESOURCE_ADDRESS_PREFIX, cfn_resource_update_call_back_function=mock_link_gateway_resource_to_gateway_resource_call_back, linking_exceptions=mock_resource_linking_exceptions(), ) @@ -2211,7 +2295,7 @@ def test_link_gateway_integration_response_to_gateway_resource( "Type": "AWS::ApiGateway::Authorizer", "Properties": {"Uri": "invoke_arn"}, }, - [LogicalIdReference("Function")], + [LogicalIdReference(value="Function", resource_type=TF_AWS_LAMBDA_FUNCTION)], { "Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Function.Arn}/invocations" }, @@ -2264,10 +2348,14 @@ def test_link_gateway_authorizer_to_lambda_function( source_resource_cfn_resource=authorizer_cfn_resources, source_resource_tf_config=authorizer_config_resources, destination_resource_tf=authorizer_tf_resources, - tf_destination_attribute_name="invoke_arn", + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=LAMBDA_FUNCTION_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="invoke_arn", + ), + ], terraform_link_field_name="authorizer_uri", cfn_link_field_name="AuthorizerUri", - terraform_resource_type_prefix=LAMBDA_FUNCTION_RESOURCE_ADDRESS_PREFIX, cfn_resource_update_call_back_function=mock_link_gateway_authorizer_to_lambda_function_call_back, linking_exceptions=mock_resource_linking_exceptions(), ) @@ -2277,7 +2365,7 @@ def test_link_gateway_authorizer_to_lambda_function( @parameterized.expand( [ ( - [LogicalIdReference("Authorizer")], + [LogicalIdReference(value="Authorizer", resource_type=TF_AWS_API_GATEWAY_AUTHORIZER)], {"Ref": "Authorizer"}, ), ( @@ -2326,10 +2414,14 @@ def test_link_gateway_authorizer_to_rest_api( source_resource_cfn_resource=authorizer_cfn_resources, source_resource_tf_config=authorizer_config_resources, destination_resource_tf=rest_api_resources, - tf_destination_attribute_name="id", + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], terraform_link_field_name="rest_api_id", cfn_link_field_name="RestApiId", - terraform_resource_type_prefix=API_GATEWAY_REST_API_RESOURCE_ADDRESS_PREFIX, cfn_resource_update_call_back_function=mock_link_resource_to_rest_api_call_back, linking_exceptions=mock_resource_linking_exceptions(), ) @@ -2366,10 +2458,14 @@ def test_link_gateway_method_to_gateway_authorizer( source_resource_cfn_resource=method_cfn_resources, source_resource_tf_config=method_config_resources, destination_resource_tf=authorizer_tf_resources, - tf_destination_attribute_name="id", + expected_destinations=[ + ResourcePairExceptedDestination( + terraform_resource_type_prefix=API_GATEWAY_AUTHORIZER_RESOURCE_ADDRESS_PREFIX, + terraform_attribute_name="id", + ), + ], terraform_link_field_name="authorizer_id", cfn_link_field_name="AuthorizerId", - terraform_resource_type_prefix=API_GATEWAY_AUTHORIZER_RESOURCE_ADDRESS_PREFIX, cfn_resource_update_call_back_function=mock_link_gateway_method_to_gateway_authorizer_call_back, linking_exceptions=mock_resource_linking_exceptions(), ) diff --git a/tests/unit/hook_packages/terraform/hooks/prepare/test_translate.py b/tests/unit/hook_packages/terraform/hooks/prepare/test_translate.py index 5ccf8bb100..56a6dbbe66 100644 --- a/tests/unit/hook_packages/terraform/hooks/prepare/test_translate.py +++ b/tests/unit/hook_packages/terraform/hooks/prepare/test_translate.py @@ -7,28 +7,31 @@ from tests.unit.hook_packages.terraform.hooks.prepare.prepare_base import PrepareHookUnitBase from samcli.hook_packages.terraform.hooks.prepare.property_builder import ( AWS_LAMBDA_FUNCTION_PROPERTY_BUILDER_MAPPING, - REMOTE_DUMMY_VALUE, AWS_API_GATEWAY_RESOURCE_PROPERTY_BUILDER_MAPPING, AWS_API_GATEWAY_REST_API_PROPERTY_BUILDER_MAPPING, AWS_API_GATEWAY_STAGE_PROPERTY_BUILDER_MAPPING, - TF_AWS_API_GATEWAY_REST_API, AWS_API_GATEWAY_METHOD_PROPERTY_BUILDER_MAPPING, + AWS_API_GATEWAY_INTEGRATION_PROPERTY_BUILDER_MAPPING, + AWS_API_GATEWAY_AUTHORIZER_PROPERTY_BUILDER_MAPPING, + AWS_API_GATEWAY_INTEGRATION_RESPONSE_PROPERTY_BUILDER_MAPPING, +) +from samcli.hook_packages.terraform.hooks.prepare.constants import ( + REMOTE_DUMMY_VALUE, TF_AWS_LAMBDA_FUNCTION, TF_AWS_LAMBDA_LAYER_VERSION, - TF_AWS_API_GATEWAY_METHOD, TF_AWS_API_GATEWAY_RESOURCE, + TF_AWS_API_GATEWAY_REST_API, TF_AWS_API_GATEWAY_STAGE, + TF_AWS_API_GATEWAY_METHOD, TF_AWS_API_GATEWAY_INTEGRATION, - AWS_API_GATEWAY_INTEGRATION_PROPERTY_BUILDER_MAPPING, TF_AWS_API_GATEWAY_AUTHORIZER, - AWS_API_GATEWAY_AUTHORIZER_PROPERTY_BUILDER_MAPPING, TF_AWS_API_GATEWAY_INTEGRATION_RESPONSE, - AWS_API_GATEWAY_INTEGRATION_RESPONSE_PROPERTY_BUILDER_MAPPING, ) from samcli.hook_packages.terraform.hooks.prepare.types import ( SamMetadataResource, LinkingPairCaller, ResourceProperties, + LinkingMultipleDestinationsOptionsCaller, ) from samcli.hook_packages.terraform.hooks.prepare.translate import ( _check_unresolvable_values, @@ -968,11 +971,18 @@ def test_check_dummy_remote_values_for_image_uri(self): def test_handle_linking(self): linking_mock_function_a = Mock() linking_mock_function_b = Mock() + linking_mock_function_c = Mock() mock_resource_links = [ LinkingPairCaller("resource_a", "resource_b", linking_mock_function_a), LinkingPairCaller("resource_b", "resource_a", linking_mock_function_b), ] + mock_multiple_destinations_resource_links = [ + LinkingMultipleDestinationsOptionsCaller( + "resource_c", ["resource_c", "resource_d"], linking_mock_function_c + ), + ] + resource_a = ResourceProperties() resource_a.cfn_resources = Mock() resource_a.terraform_resources = Mock() @@ -983,13 +993,29 @@ def test_handle_linking(self): resource_b.terraform_resources = Mock() resource_b.terraform_config = Mock() + resource_c = ResourceProperties() + resource_c.cfn_resources = Mock() + resource_c.terraform_resources = {"res_c_logical_id": Mock()} + resource_c.terraform_config = Mock() + + resource_d = ResourceProperties() + resource_d.cfn_resources = Mock() + resource_d.terraform_resources = {"res_d_logical_id": Mock()} + resource_d.terraform_config = Mock() + resource_property_mapping = { "resource_a": resource_a, "resource_b": resource_b, + "resource_c": resource_c, + "resource_d": resource_d, } with patch("samcli.hook_packages.terraform.hooks.prepare.translate.RESOURCE_LINKS", mock_resource_links): - _handle_linking(resource_property_mapping) + with patch( + "samcli.hook_packages.terraform.hooks.prepare.translate.MULTIPLE_DESTINATIONS_RESOURCE_LINKS", + mock_multiple_destinations_resource_links, + ): + _handle_linking(resource_property_mapping) linking_mock_function_a.assert_called_once_with( resource_a.terraform_config, resource_a.cfn_resources, resource_b.terraform_resources @@ -998,6 +1024,15 @@ def test_handle_linking(self): resource_b.terraform_config, resource_b.cfn_resources, resource_a.terraform_resources ) + linking_mock_function_c.assert_called_once_with( + resource_c.terraform_config, + resource_c.cfn_resources, + { + **resource_c.terraform_resources, + **resource_d.terraform_resources, + }, + ) + def test_get_s3_object_hash(self): self.assertEqual( _get_s3_object_hash(self.s3_bucket, self.s3_key), _get_s3_object_hash(self.s3_bucket, self.s3_key)