def _list_from_aws_result_to_resources( cls: Type["AWSResourceSpec"], list_from_aws_result: ListFromAWSResult, context: Dict[str, str], ) -> List[Resource]: resources: List[Resource] = [] for arn, resource_dict in list_from_aws_result.resources.items(): try: partial_resource_link_collection = cls.schema.parse( resource_dict, context) except Exception as ex: raise SchemaParseException(( f"Error parsing {cls.__name__} : " f"{arn}:\n\nData: {resource_dict}\n\nError: {ex}")) from ex resource_account_id = arn.split(":")[4] account_region_links = [] if resource_account_id: if resource_account_id != "aws": account_link = ResourceLink( pred="account", obj=f"arn:aws::::account/{resource_account_id}") account_region_links.append(account_link) resource_region_name = arn.split(":")[3] if resource_region_name: region_link = ResourceLink( pred="region", obj= f"arn:aws:::{resource_account_id}:region/{resource_region_name}", ) account_region_links.append(region_link) link_collection = partial_resource_link_collection + LinkCollection.from_links( account_region_links) resource = Resource( resource_id=arn, type=cls.get_full_type_name(), link_collection=link_collection, ) resources.append(resource) return resources
def merge_resources(resource_id: str, resources: List[Resource]) -> Resource: """Merge multiple resources with the same id into one. This is permissible in two cases: 1) If all resources have the same value for 'get_full_type' and no key/values conflict they will be merged into a single resource by combining all key/values into a single resource 2) If all resources do not have the same value for 'get_full_type', if classes have the allow_clobber attribute set depending on the values a resource may be created. Args: resource_id: common resource id resources: list of Resources to merge Returns: merged Resource object Raises: UnmergableDuplicateResourceIdsFoundException if resources could not be merged. """ full_type_names = {resource.type for resource in resources} if len(full_type_names) > 1: # in this case conflicting resources have different full_type_names, this # can be a permissible if allow_clobber is used. Here we determine this by # building a dict where the keys are resource spec classes which have # appeared in the duplicate resources allow_clobber list and the # values are the classes where they appeared. If at the end in # this dict there are any values which match the complete list of # spec classes, we use the resource of that type spec_classes_resources: DefaultDict[ Type[ResourceSpec], List[Resource]] = defaultdict(list) allow_clobber_classes_classes: DefaultDict[ Type[ResourceSpec], Set[Type[ResourceSpec]]] = defaultdict(set) for resource in resources: full_type_name = resource.type spec_classes: List[ Type[ResourceSpec]] = ResourceSpec.get_by_full_type_name( full_type_name) for spec_class in spec_classes: spec_classes_resources[spec_class].append(resource) for allow_clobber_class in spec_class.allow_clobber: allow_clobber_classes_classes[allow_clobber_class].add( spec_class) all_spec_classes = set(spec_classes_resources.keys()) winning_class = None for allow_clobber_class, classes in allow_clobber_classes_classes.items( ): if len(classes) == len(all_spec_classes) - 1: winning_class = (all_spec_classes - classes).pop() if not winning_class: raise UnmergableDuplicateResourceIdsFoundException( (f"Multiple resources for {resource_id} with " f"different types that aren't clobberable: " f"{[resource.dict() for resource in resources]}")) resources = spec_classes_resources[winning_class] full_type_names = {resource.type for resource in resources} merged_resource_type_name = full_type_names.pop() merged_link_keys_links: Dict[str, Link] = {} for duplicate_resource in resources: for link in duplicate_resource.link_collection.get_links(): duplicate_link = merged_link_keys_links.get(link.pred) if duplicate_link: if type(duplicate_link) != type(link): raise UnmergableDuplicateResourceIdsFoundException( f"Conflicting link types {type(link)}, {type(duplicate_link)} found in duplicate #s {resources}" ) if duplicate_link.obj != link.obj: raise UnmergableDuplicateResourceIdsFoundException( f"Conflicting link values {link.obj}, {duplicate_link.obj} found in duplicate #s {resources}" ) else: merged_link_keys_links[link.pred] = link merged_links = list(merged_link_keys_links.values()) link_collection = LinkCollection.from_links(merged_links) return Resource( resource_id=resource_id, type=merged_resource_type_name, link_collection=link_collection, )