def _mount_references(self): ''' Find the references that is defined in self.configuration :return: ''' self.resources_raw = deepcopy(self.resources) invalid_references = ('var.', 'each.') # This section will link resources found in configuration part of the plan output. # The reference should be on both ways (A->B, B->A) since terraform sometimes report these references # in opposite ways, depending on the provider structure. for resource in self.configuration['resources']: if 'expressions' in self.configuration['resources'][resource]: ref_list = {} for key, value in self.configuration['resources'][resource][ 'expressions'].items(): references = seek_key_in_dict( value, 'references') if isinstance( value, (dict, list)) else [] valid_references = [] for ref in references: if isinstance(ref, dict) and ref.get('references'): valid_references = [ r for r in ref['references'] if not r.startswith(invalid_references) ] for ref in valid_references: # if ref is not in the correct format, handle it if len(ref.split('.')) < 3 and ref.startswith( 'module'): # Using for_each and modules together may introduce an issue where the plan.out.json won't include the necessary third part of the reference # It is partially resolved by mounting the reference to all instances belonging to the module if 'for_each_expression' in self.configuration[ 'resources'][resource]: # extract source resources assumed_source_resources = [ k for k in self.resources.keys() if k.startswith(resource) ] # extract for_each keys assumed_for_each_keys = [ k[len(resource):].split('.')[0] for k in assumed_source_resources ] # combine ref with for each keys assumed_refs = [ '{}{}'.format(ref, key) for key in assumed_for_each_keys ] # get all the resources that start with updated ref ambigious_references = [] for r in self.resources.keys(): for assumed_ref in assumed_refs: if r.startswith(assumed_ref): if key in ref_list: ref_list[key].append(r) else: ref_list[key] = [r] ambigious_references.append(r) # throw a warning defaults = Defaults() console_write('{} {}: {}'.format( defaults.warning_icon, defaults.warning_colour( 'WARNING (Mounting)'), defaults.info_colour( 'The reference "{}" in resource {} is ambigious.' ' It will be mounted to the following resources:' ).format(ref, resource))) for i, r in enumerate(ambigious_references, 1): console_write( defaults.info_colour('{}. {}'.format( i, r))) # if the reference can not be resolved, warn the user and continue. else: console_write('{} {}: {}'.format( Defaults().warning_icon, Defaults().warning_colour( 'WARNING (Mounting)'), Defaults().info_colour( 'The reference "{}" in resource {} is ambigious. It will not be mounted.' .format(ref, resource)))) continue elif key not in ref_list: ref_list[key] = self._find_resource_from_name(ref) else: ref_list[key].extend( self._find_resource_from_name(ref)) # This is where we synchronise constant_value in the configuration section with the resource # for filling up the missing elements that hasn't been defined in the resource due to provider # implementation. target_resource = [ t for t in [self.resources.get(resource, {}).get('address')] if t is not None ] if not target_resource: target_resource = [ k for k in self.resources.keys() if k.startswith(resource) ] for t_r in target_resource: if type(value) is type( self.resources[t_r]['values'].get(key) ) and self.resources[t_r]['values'].get(key) != value: if isinstance(value, (list, dict)): merge_dicts(self.resources[t_r]['values'][key], value) if ref_list: ref_type = self.configuration['resources'][resource][ 'expressions'].get('type', {}) if 'references' in ref_type: ref_type = resource.split('.')[0] if not ref_type and not self.is_type(resource, 'data'): ref_type = self.extract_resource_type_from_address( resource) for k, v in ref_list.items(): v = flatten_list(v) # Mounting A->B source_resources = self._find_resource_from_name( self.configuration['resources'][resource]['address']) self._mount_resources(source=source_resources, target=ref_list, ref_type=ref_type) # Mounting B->A for parameter, target_resources in ref_list.items(): for target_resource in target_resources: if not self.is_type( resource, 'data') and not self.is_type( resource, 'var') and not self.is_type( resource, 'provider'): ref_type = self.extract_resource_type_from_address( target_resource) self._mount_resources( source=[target_resource], target={parameter: source_resources}, ref_type=ref_type)
def _mount_resources(self, source, target, ref_type): ''' Mounts values of the source resource to the target resource's values with ref_type key :param source: source resource :param target: target resource :param ref_type: reference type (e.g. ingress ) :return: none ''' for source_resource in source: if 'values' not in self.resources.get(source_resource, {}): continue for parameter, target_resources in target.items(): for target_resource in target_resources: if target_resource not in self.resources or 'values' not in self.resources[ target_resource]: continue resource = self.resources_raw[source_resource]['values'] # This is a very stupid terraform-provider bug. Somehow, sometimes it loses the state # and sets the value to None - which is normally not allowed.. It should have been an empty # dict instead. Hence, we are fixing that here. if resource is None: defaults = Defaults() console_write('{} {}: {}'.format( defaults.warning_icon, defaults.warning_colour('WARNING (mounting)'), defaults.info_colour( 'The resource "{}" has no values set. This is a terraform provider ' 'bug. Its recommended to remove/fix this resource within your state.' .format(source_resource)))) self.resources_raw[source_resource]['values'] = {} self.resources[source_resource]['values'] = {} resource = {} resource[Defaults.mounted_ptr] = True if Defaults.r_mount_ptr not in self.resources[ target_resource]: self.resources[target_resource][ Defaults.r_mount_ptr] = {} if Defaults.r_mount_addr_ptr not in self.resources[ target_resource]: self.resources[target_resource][ Defaults.r_mount_addr_ptr] = {} if Defaults.r_mount_addr_ptr_list not in self.resources[ target_resource]: self.resources[target_resource][ Defaults.r_mount_addr_ptr_list] = [] # ensure resources[target_resource]['values'] is an # empty dict and not None if not self.resources[target_resource]['values']: self.resources[target_resource]['values'] = dict() if ref_type not in self.resources[target_resource][ 'values']: self.resources[target_resource]['values'][ ref_type] = [] self.resources[target_resource]['values'][ref_type].append( resource) self.resources[target_resource][ Defaults.r_mount_ptr][parameter] = ref_type self.resources[target_resource][ Defaults.r_mount_addr_ptr][parameter] = source target_set = set(self.resources[target_resource][ Defaults.r_mount_addr_ptr_list]) source_set = set(source) self.resources[target_resource][ Defaults.r_mount_addr_ptr_list] = list(target_set | source_set) if parameter not in self.resources[source_resource][ 'values']: self.resources[source_resource]['values'][ parameter] = target_resource