app/models/provider_tag_mapping/mapper.rb
# coding: utf-8
class ProviderTagMapping
# Performs most of the work of ProviderTagMapping - holds current mappings,
# computes applicable tags, and creates/finds Tag records - except actually [un]assigning.
class Mapper
# @return [InventoryCollection<Tag>] a collection saving which will find/create (never delete) all
# tags referenced by #map_labels whose id is not yet known.
attr_reader :tags_to_resolve_collection
# @return [InventoryCollection<Tag>] represents tags whose id is already known.
# Doesn't require saving, not really interesting.
attr_reader :specific_tags_collection
attr_reader :parameters
# @param mappings [Array<ProviderTagMapping>] Mapping records to use
def initialize(mappings, mapper_parameters = {})
@parameters = mapper_parameters || {}
# {[name, type, value] => [tag_id, ...]}
@mappings = mappings.group_by { |m| [case_sensitive_labels? ? m.label_name : m.label_name&.downcase, m.labeled_resource_type, m.label_value].freeze }
.transform_values { |ms| ms.collect(&:tag_id) }
require "inventory_refresh"
@tags_to_resolve_collection = ::InventoryRefresh::InventoryCollection.new(
:name => :mapped_tags_to_resolve,
:model_class => Tag,
# more than needed to identify, doesn't matter much as we use custom save
:manager_ref => [:category_tag_id, :entry_name, :entry_description],
# :arel => Tag.all,
:custom_save_block => lambda do |_ems, inv_collection|
# TODO: O(N) queries, optimize.
inv_collection.each do |inv_object|
inv_object.id ||= find_or_create_tag(inv_object.attributes)
end
end
)
@specific_tags_collection = ::InventoryRefresh::InventoryCollection.new(
:name => :mapped_specific_tags,
:model_class => Tag,
:manager_ref => [:id]
)
end
def case_sensitive_labels?
@parameters[:case_sensitive_labels]
end
def cached_filter_single_value_category_tag_ids(category_tag_ids)
@single_value_category_tag_ids ||= []
@multiple_value_category_tag_ids ||= []
tag_ids = category_tag_ids - @single_value_category_tag_ids - @multiple_value_category_tag_ids
if tag_ids.present? # some tag ids are not cached yet
single_value_tag_ids = Classification.where(:tag_id => tag_ids, :single_value => true).pluck(:tag_id)
@single_value_category_tag_ids.concat(single_value_tag_ids)
@multiple_value_category_tag_ids.concat(tag_ids - single_value_tag_ids)
end
@single_value_category_tag_ids & category_tag_ids
end
# Compute desired tags, in intermediate form to be resolved later.
#
# @param type [String] Matched against `labeled_resource_type` in mappings.
# May be `resource_type` of an actual label, but doesn't have to; can be fake string such as 'Vm'.
# @param labels [Array] array of {:name, :value} hashes.
# @return [Array<InventoryObject>] representing desired tags.
def map_labels(type, labels)
inventory_objects = labels.collect_concat { |label| map_label(type, label) }.uniq
inventory_objects_by_category = inventory_objects.group_by { |inventory_object| inventory_object[:category_tag_id] }
single_value_tag_ids = cached_filter_single_value_category_tag_ids(inventory_objects_by_category.keys)
inventory_objects_by_category.map do |category_tag_id, grouped_inventory_objects|
if single_value_tag_ids.include?(category_tag_id)
selected_inventory_object = grouped_inventory_objects.min_by { |x| x.data.fetch(:entry_name) }
if grouped_inventory_objects.count > 1
$log.warn("Label to Tag Mapper has encountered multiple mappings for the only single value tag category [Classification##{category_tag_id}]")
possible_labels = grouped_inventory_objects.map { |x| x.data.fetch(:entry_description) }.join(', ')
$log.warn("Only selected label value [#{selected_inventory_object.data.fetch(:entry_description)}] is going to be mapped (possible labels [#{possible_labels}]).")
end
selected_inventory_object
else
grouped_inventory_objects
end
end.flatten.compact
end
# Convert "tag references" to actual Tag objects. Must have been resolved to known id first.
# @param tag_references [Array<InventoryObject>]
# @return [Array<Tag>]
def self.references_to_tags(tag_references)
ref_without_id = tag_references.detect { |ref| ref.id.nil? }
raise "Unresolved tag reference #{ref_without_id}, must save tags_to_resolve_collection first" if ref_without_id
Tag.find(tag_references.collect(&:id))
end
private
def map_label(type, label)
label_name = case_sensitive_labels? ? label[:name] : label[:name]&.downcase
label_value = label[:value].strip
# Apply both specific-type and any-type, independently.
(map_name_type_value(label_name, type, label_value) +
map_name_type_value(label_name, nil, label_value) +
map_name_type_value(label_name, "_all_entities_", label_value))
end
def map_name_type_value(name, type, value)
specific_value = @mappings[[name, type, value]] || []
any_value = @mappings[[name, type, nil]] || []
if !specific_value.empty?
specific_value.map { |tag_id| emit_specific_reference(tag_id) }
else
if value.empty?
[] # Don't map empty value to any tag.
else
# Note: if the way we compute `entry_name` changes,
# consider what will happen to previously created tags.
any_value.map do |tag_id|
emit_tag_reference(
:category_tag_id => tag_id,
:entry_name => Classification.sanitize_name(value),
:entry_description => value
)
end
end
end
end
def emit_tag_reference(h)
tags_to_resolve_collection.find_or_build_by(h)
end
def emit_specific_reference(tag_id)
inv_object = specific_tags_collection.find_or_build_by(:id => tag_id)
inv_object.id = tag_id
inv_object
end
# @return [Integer] Tag id
# Mutate the hash to contain :tag_id.
def find_or_create_tag(tag_hash)
category = Tag.find(tag_hash[:category_tag_id]).classification
entry = category.find_entry_by_name(tag_hash[:entry_name])
unless entry
category.lock(:exclusive) do
begin
entry = category.add_entry(:name => tag_hash[:entry_name],
:description => tag_hash[:entry_description])
entry.save!
rescue ActiveRecord::RecordInvalid
entry = category.find_entry_by_name(tag_hash[:entry_name])
end
end
end
entry.tag_id
end
end
end