from collections import namedtuple from typing import Any, Dict, Iterable, List, Optional from typing_extensions import TypeGuard from samtranslator.model import ResourceResolver from samtranslator.model.apigateway import ApiGatewayRestApi from samtranslator.model.apigatewayv2 import ApiGatewayV2HttpApi from samtranslator.model.connector_profiles.profile import replace_cfn_resource_properties from samtranslator.model.dynamodb import DynamoDBTable from samtranslator.model.intrinsics import get_logical_id_from_intrinsic, ref from samtranslator.model.lambda_ import ( LambdaFunction, ) from samtranslator.model.stepfunctions import StepFunctionsStateMachine from samtranslator.public.sdk.resource import SamResourceType from samtranslator.utils.utils import as_array, insert_unique # TODO: Switch to dataclass ConnectorResourceReference = namedtuple( "ConnectorResourceReference", [ "logical_id", "resource_type", "arn", "role_name", "queue_url", "resource_id", "name", "qualifier", ], ) _SAM_TO_CFN_RESOURCE_TYPE = { SamResourceType.Function.value: LambdaFunction.resource_type, SamResourceType.StateMachine.value: StepFunctionsStateMachine.resource_type, SamResourceType.Api.value: ApiGatewayRestApi.resource_type, SamResourceType.HttpApi.value: ApiGatewayV2HttpApi.resource_type, SamResourceType.SimpleTable.value: DynamoDBTable.resource_type, } UNSUPPORTED_CONNECTOR_PROFILE_TYPE = "UNSUPPORTED_CONNECTOR_PROFILE_TYPE" class ConnectorResourceError(Exception): """ Indicates a template error making a resource unusable for connectors. """ def _is_nonblank_str(s: Any) -> TypeGuard[str]: return s and isinstance(s, str) def add_depends_on(logical_id: str, depends_on: str, resource_resolver: ResourceResolver) -> None: """ Add DependsOn attribute to resource. """ resource = resource_resolver.get_resource_by_logical_id(logical_id) if not resource: return old_deps = resource.get("DependsOn", []) deps = insert_unique(old_deps, depends_on) resource["DependsOn"] = deps def replace_depends_on_logical_id(logical_id: str, replacement: List[str], resource_resolver: ResourceResolver) -> None: """ For every resource's `DependsOn`, replace `logical_id` by `replacement`. """ for resource in resource_resolver.get_all_resources().values(): depends_on = as_array(resource.get("DependsOn", [])) if logical_id in depends_on: depends_on.remove(logical_id) resource["DependsOn"] = insert_unique(depends_on, replacement) def get_event_source_mappings( event_source_id: str, function_id: str, resource_resolver: ResourceResolver ) -> Iterable[str]: """ Get logical IDs of `AWS::Lambda::EventSourceMapping`s between resource logical IDs. """ resources = resource_resolver.get_all_resources() for logical_id, resource in resources.items(): if resource.get("Type") == "AWS::Lambda::EventSourceMapping": properties = resource.get("Properties", {}) # Not taking intrinsics as input to function as FunctionName could be a number of # formats, which would require parsing it anyway resource_function_id = get_logical_id_from_intrinsic(properties.get("FunctionName")) resource_event_source_id = get_logical_id_from_intrinsic(properties.get("EventSourceArn")) if ( resource_function_id and resource_event_source_id and function_id == resource_function_id and event_source_id == resource_event_source_id ): yield logical_id def _is_valid_resource_reference(obj: Dict[str, Any]) -> bool: id_provided = "Id" in obj # Every property in ResourceReference can be implied using 'Id', except for 'Qualifier', so users should be able to combine 'Id' and 'Qualifier' non_id_provided = len([k for k in obj if k not in ["Id", "Qualifier"]]) > 0 # Must provide Id (with optional Qualifier) or a supported combination of other properties. return id_provided != non_id_provided def get_resource_reference( obj: Dict[str, Any], resource_resolver: ResourceResolver, connecting_obj: Dict[str, Any] ) -> ConnectorResourceReference: if not _is_valid_resource_reference(obj): raise ConnectorResourceError( "Must provide 'Id' (with optional 'Qualifier') or a supported combination of other properties." ) logical_id = obj.get("Id") # Must provide Id (with optional Qualifier) or a supported combination of other properties # If Id is not provided, all values must come from overrides. if not logical_id: resource_type = obj.get("Type") if not _is_nonblank_str(resource_type): raise ConnectorResourceError("'Type' is missing or not a string.") # profiles.json only support CFN resource type. # We need to convert SAM resource types to corresponding CFN resource type resource_type = _SAM_TO_CFN_RESOURCE_TYPE.get(resource_type, resource_type) return ConnectorResourceReference( logical_id=None, resource_type=resource_type, arn=obj.get("Arn"), role_name=obj.get("RoleName"), queue_url=obj.get("QueueUrl"), resource_id=obj.get("ResourceId"), name=obj.get("Name"), qualifier=obj.get("Qualifier"), ) if not _is_nonblank_str(logical_id): raise ConnectorResourceError("'Id' is missing or not a string.") resource = resource_resolver.get_resource_by_logical_id(logical_id) if not resource: raise ConnectorResourceError(f"Unable to find resource with logical ID '{logical_id}'.") resource_type = resource.get("Type") if not _is_nonblank_str(resource_type): raise ConnectorResourceError("'Type' is missing or not a string.") properties = resource.get("Properties", {}) cfn_resource_properties = replace_cfn_resource_properties(resource_type, logical_id) cfn_resource_properties_output = cfn_resource_properties.get("Outputs", {}) arn = _get_resource_arn(cfn_resource_properties_output) role_name = _get_resource_role_name( connecting_obj.get("Id"), connecting_obj.get("Arn"), cfn_resource_properties, properties ) queue_url = _get_resource_queue_url(cfn_resource_properties_output) resource_id = _get_resource_id(cfn_resource_properties_output) name = _get_resource_name(cfn_resource_properties_output) qualifier = obj.get("Qualifier") if "Qualifier" in obj else _get_resource_qualifier(cfn_resource_properties_output) return ConnectorResourceReference( logical_id=logical_id, resource_type=resource_type, arn=arn, role_name=role_name, queue_url=queue_url, resource_id=resource_id, name=name, qualifier=qualifier, ) def _get_events_rule_role( connecting_obj_id: Optional[str], connecting_obj_arn: Optional[Any], properties: Dict[str, Any] ) -> Optional[Any]: for target in properties.get("Targets", []): target_arn = target.get("Arn") target_logical_id = get_logical_id_from_intrinsic(target_arn) if (target_logical_id and target_logical_id == connecting_obj_id) or ( connecting_obj_arn and target_arn == connecting_obj_arn ): return target.get("RoleArn") return None def _get_resource_role_property( connecting_obj_id: Optional[str], connecting_obj_arn: Optional[Any], cfn_resource_properties: Dict[str, Any], properties: Dict[str, Any], ) -> Any: role_property = cfn_resource_properties.get("Inputs", {}).get("Role") if isinstance(role_property, str): return properties.get(role_property) if isinstance(role_property, dict) and role_property.get("Function") == "GetEventsRuleRole": return _get_events_rule_role(connecting_obj_id, connecting_obj_arn, properties) return None def _get_resource_role_name( connecting_obj_id: Optional[str], connecting_obj_arn: Optional[Any], cfn_resource_properties: Dict[str, Any], properties: Dict[str, Any], ) -> Any: role = _get_resource_role_property(connecting_obj_id, connecting_obj_arn, cfn_resource_properties, properties) if not role: return None logical_id = get_logical_id_from_intrinsic(role) if not logical_id: return None return ref(logical_id) def _get_resource_queue_url(properties: Dict[str, Any]) -> Optional[Any]: return properties.get("Url") def _get_resource_id(properties: Dict[str, Any]) -> Optional[Any]: return properties.get("Id") def _get_resource_name(properties: Dict[str, Any]) -> Optional[Any]: return properties.get("Name") def _get_resource_qualifier(properties: Dict[str, Any]) -> Optional[Any]: # Qualifier is used as the execute-api ARN suffix; by default allow whole API return properties.get("Qualifier") def _get_resource_arn(properties: Dict[str, Any]) -> Any: # according to documentation, Ref returns ARNs for these two resource types # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html#aws-resource-stepfunctions-statemachine-return-values # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sns-topic.html#aws-resource-sns-topic-return-values # For all other supported resources, we can typically use Fn::GetAtt LogicalId.Arn to obtain ARNs return properties.get("Arn")