From 7c76be482a4350da678adbabc50864e336dfda6f Mon Sep 17 00:00:00 2001 From: Beni Cherniavsky-Paskin Date: Mon, 10 Jul 2017 14:27:57 +0300 Subject: [PATCH] Implement most entity links in get_*_graph Now kubernetes graph refresh passes full refresher tests! - Except tagging by labels which is skipped. A few things starting with _ indicate still missing functionality that is not covered by refresher_spec, will add tests later. Mostly using lazy_find by secodary indexes on name or (namespace, name). - Still using data_index for images & registries. - Used a local hash similar to data_index for endpoint/service matching. --- .../inventory_collections.rb | 104 ++++++--- .../container_manager/refresh_parser.rb | 205 +++++++++++++++--- .../container_manager/refresher_spec.rb | 39 ++-- 3 files changed, 269 insertions(+), 79 deletions(-) diff --git a/app/models/manageiq/providers/kubernetes/container_manager/inventory_collections.rb b/app/models/manageiq/providers/kubernetes/container_manager/inventory_collections.rb index 867d79852d..82e3362a8f 100644 --- a/app/models/manageiq/providers/kubernetes/container_manager/inventory_collections.rb +++ b/app/models/manageiq/providers/kubernetes/container_manager/inventory_collections.rb @@ -6,27 +6,28 @@ def initialize_inventory_collections(ems) :model_class => ContainerProject, :parent => ems, :builder_params => {:ems_id => ems.id}, - :association => :container_projects + :association => :container_projects, + :secondary_refs => {:by_name => [:name]}, ) @inv_collections[:container_quotas] = ::ManagerRefresh::InventoryCollection.new( - :model_class => ContainerQuota, - :parent => ems, - :builder_params => {:ems_id => ems.id}, - :association => :container_quotas, - #:arel => ContainerQuota.joins(:container_project).where(:container_projects => {:ems_id => ems.id}), + :model_class => ContainerQuota, + :parent => ems, + :builder_params => {:ems_id => ems.id}, + :association => :container_quotas, + :attributes_blacklist => [:namespace], ) @inv_collections[:container_quota_items] = ::ManagerRefresh::InventoryCollection.new( :model_class => ContainerQuotaItem, :parent => ems, :association => :container_quota_items, - #:arel => ContainerQuotaItem.joins(:container_quota => :container_project).where(:container_projects => {:ems_id => ems.id}), :manager_ref => [:container_quota, :resource], ) @inv_collections[:container_limits] = ::ManagerRefresh::InventoryCollection.new( - :model_class => ContainerLimit, - :parent => ems, - :builder_params => {:ems_id => ems.id}, - :association => :container_limits, + :model_class => ContainerLimit, + :parent => ems, + :builder_params => {:ems_id => ems.id}, + :association => :container_limits, + :attributes_blacklist => [:namespace], ) @inv_collections[:container_limit_items] = ::ManagerRefresh::InventoryCollection.new( :model_class => ContainerLimitItem, @@ -39,7 +40,10 @@ def initialize_inventory_collections(ems) :parent => ems, :builder_params => {:ems_id => ems.id}, :association => :container_nodes, + :secondary_refs => {:by_name => [:name]}, ) + initialize_container_conditions_collection(ems.container_nodes) + initialize_custom_attributes_collections(ems.container_nodes, %w(labels additional_attributes)) # polymorphic child of ContainerNode & ContainerImage, # but refresh only sets it on nodes. @@ -79,16 +83,22 @@ def initialize_inventory_collections(ems) :parent => ems, :builder_params => {:ems_id => ems.id}, :association => :container_images, - :manager_ref => [:image_ref, :container_image_registry], + # TODO: old save matches on [:image_ref, :container_image_registry_id] + # TODO: should match on digest when available + :manager_ref => [:image_ref], ) @inv_collections[:container_groups] = ::ManagerRefresh::InventoryCollection.new( - :model_class => ContainerGroup, - :parent => ems, - :builder_params => {:ems_id => ems.id}, - :association => :container_groups, + :model_class => ContainerGroup, + :parent => ems, + :builder_params => {:ems_id => ems.id}, + :association => :container_groups, + :secondary_refs => {:by_namespace_and_name => [:namespace, :name]}, + :attributes_blacklist => [:namespace], ) + initialize_container_conditions_collection(ems.container_groups) + initialize_custom_attributes_collections(ems.container_groups, %w(labels node_selectors)) @inv_collections[:container_definitions] = ::ManagerRefresh::InventoryCollection.new( :model_class => ContainerDefinition, @@ -137,18 +147,25 @@ def initialize_inventory_collections(ems) @inv_collections[:container_replicators] = ::ManagerRefresh::InventoryCollection.new( - :model_class => ContainerReplicator, - :parent => ems, - :builder_params => {:ems_id => ems.id}, - :association => :container_replicators, + :model_class => ContainerReplicator, + :parent => ems, + :builder_params => {:ems_id => ems.id}, + :association => :container_replicators, + :secondary_refs => {:by_namespace_and_name => [:namespace, :name]}, + :attributes_blacklist => [:namespace], ) + initialize_custom_attributes_collections(ems.container_replicators, %w(labels selectors)) + @inv_collections[:container_services] = ::ManagerRefresh::InventoryCollection.new( - :model_class => ContainerService, - :parent => ems, - :builder_params => {:ems_id => ems.id}, - :association => :container_services, + :model_class => ContainerService, + :parent => ems, + :builder_params => {:ems_id => ems.id}, + :association => :container_services, + :secondary_refs => {:by_namespace_and_name => [:namespace, :name]}, + :attributes_blacklist => [:namespace], ) + initialize_custom_attributes_collections(ems.container_services, %w(labels selectors)) @inv_collections[:container_service_port_configs] = ::ManagerRefresh::InventoryCollection.new( :model_class => ContainerServicePortConfig, @@ -172,10 +189,11 @@ def initialize_inventory_collections(ems) ) @inv_collections[:container_templates] = ::ManagerRefresh::InventoryCollection.new( - :model_class => ContainerTemplate, - :parent => ems, - :builder_params => {:ems_id => ems.id}, - :association => :container_templates, + :model_class => ContainerTemplate, + :parent => ems, + :builder_params => {:ems_id => ems.id}, + :association => :container_templates, + :attributes_blacklist => [:namespace], ) @inv_collections[:container_template_parameters] = ::ManagerRefresh::InventoryCollection.new( @@ -199,7 +217,7 @@ def initialize_inventory_collections(ems) :association => :container_build_pods, # TODO: is this unique? build pods do have uid that becomes ems_ref, # but we need lazy_find by name for lookup from container_group - # TODO rename namespace -> container_project column? + # TODO: rename namespace -> container_project column? :manager_ref => [:namespace, :name], ) @inv_collections[:persistent_volumes] = @@ -217,4 +235,32 @@ def initialize_inventory_collections(ems) :association => :persistent_volume_claims, ) end + + # ContainerCondition is polymorphic child of ContainerNode & ContainerGroup. + def initialize_container_conditions_collection(relation) + query = ContainerCondition.where( + :container_entity_type => relation.model.name, + :container_entity_id => relation, # nested SELECT. TODO: compare to a JOIN. + ) + @inv_collections[[:container_conditions_for, relation.model.name]] = + ::ManagerRefresh::InventoryCollection.new( + :model_class => ContainerCondition, + :arel => query, + :manager_ref => [:container_entity, :name], + ) + end + + # CustomAttribute is polymorphic child of many models + def initialize_custom_attributes_collections(relation, sections) + sections.each do |section| + query = CustomAttribute.where(:resource_type => relation.model.name, + :resource_id => relation, + :section => section.to_s) + @inv_collections[[:custom_attributes_for, relation.model.name, section]] = ::ManagerRefresh::InventoryCollection.new( + :model_class => CustomAttribute, + :arel => query, + :manager_ref => [:resource, :section, :name], + ) + end + end end diff --git a/app/models/manageiq/providers/kubernetes/container_manager/refresh_parser.rb b/app/models/manageiq/providers/kubernetes/container_manager/refresh_parser.rb index 89f6f14f1c..25069078fc 100644 --- a/app/models/manageiq/providers/kubernetes/container_manager/refresh_parser.rb +++ b/app/models/manageiq/providers/kubernetes/container_manager/refresh_parser.rb @@ -36,11 +36,14 @@ def ems_inv_to_hashes(inventory, _options = Config::Options.new) get_services(inventory) get_component_statuses(inventory) EmsRefresh.log_inv_debug_trace(@data, "data:") + + # Returning a hash triggers save_inventory_container code path. @data end def ems_inv_to_inv_collections(ems, inventory, _options = Config::Options.new) initialize_inventory_collections(ems) + get_additional_attributes_graph(inventory) # TODO: untested? get_nodes_graph(inventory) get_namespaces_graph(inventory) get_resource_quotas_graph(inventory) @@ -49,9 +52,14 @@ def ems_inv_to_inv_collections(ems, inventory, _options = Config::Options.new) get_persistent_volume_claims_graph(inventory) get_persistent_volumes_graph(inventory) get_pods_graph(inventory) - get_services_graph(inventory) + get_endpoints_and_services_graph(inventory) get_component_statuses_graph(inventory) + # The following use images resulting from parsing pods, so must be called after. + # TODO: openshift images parsing will have to plug before this. + get_container_images_graph + get_container_image_registries_graph + # Returning an array triggers ManagerRefresh::SaveInventory code path. @inv_collections.values end @@ -205,6 +213,16 @@ def get_additional_attributes(inventory) ## InventoryObject Refresh methods + def get_additional_attributes_graph(inv) + (inv["additional_attributes"] || {}).each do |aa| + h = parse_additional_attribute(aa) + next if h.empty? || h[:node].nil? + + container_node = lazy_find_node(:name => h.delete(:node)) + get_custom_attributes_graph(container_node, :additional_attributes => [h]) + end + end + def get_nodes_graph(inv) collection = @inv_collections[:container_nodes] @@ -213,20 +231,17 @@ def get_nodes_graph(inv) h.except!(:namespace, :tags) - _custom_attrs = h.extract!(:labels, :additional_attributes) + labels = h.delete(:labels) children = h.extract!(:container_conditions, :computer_system) - node = collection.build(h) + container_node = collection.build(h) - get_node_container_conditions_graph(node, children[:container_conditions]) - get_node_computer_systems_graph(node, children[:computer_system]) + get_container_conditions_graph(container_node, children[:container_conditions]) + get_node_computer_systems_graph(container_node, children[:computer_system]) + get_custom_attributes_graph(container_node, :labels => labels) end end - def get_node_container_conditions_graph(parent, hashes) - # TODO - end - def get_node_computer_systems_graph(parent, hash) return if hash.nil? @@ -258,10 +273,11 @@ def get_namespaces_graph(inv) h = parse_namespace(ns) h.except!(:tags) + custom_attrs = h.extract!(:labels) - _custom_attrs = h.extract!(:labels) + container_project = collection.build(h) - collection.build(h) + get_custom_attributes_graph(container_project, custom_attrs) end end @@ -271,7 +287,7 @@ def get_resource_quotas_graph(inv) inv["resource_quota"].each do |quota| h = parse_resource_quota(quota) - h[:container_project] = lazy_find_project(h.delete(:project)) + h[:container_project] = lazy_find_project(:name => h[:namespace]) items = h.delete(:container_quota_items) get_container_quota_items_graph(h, items) @@ -294,7 +310,7 @@ def get_limit_ranges_graph(inv) inv["limit_range"].each do |data| h = parse_range(data) - h[:container_project] = lazy_find_project(h.delete(:project)) + h[:container_project] = lazy_find_project(:name => h[:namespace]) items = h.delete(:container_limit_items) limit = collection.build(h) @@ -317,12 +333,16 @@ def get_replication_controllers_graph(inv) inv["replication_controller"].each do |rc| h = parse_replication_controllers(rc) - h.except!(:namespace, :tags) + h.except!(:tags) - h[:container_project] = lazy_find_project(h.delete(:project)) - _custom_attrs = h.extract!(:labels, :selector_parts) + h[:container_project] = lazy_find_project(:name => h[:namespace]) + custom_attrs = h.extract!(:labels, :selector_parts) - collection.build(h) + container_replicator = collection.build(h) + get_custom_attributes_graph(container_replicator, + :labels => custom_attrs[:labels], + # The actual section is "selectors" + :selectors => custom_attrs[:selector_parts]) end end @@ -344,8 +364,13 @@ def get_persistent_volumes_graph(inv) inv["persistent_volume"].each do |pv| h = parse_persistent_volume(pv) - h.except!(:namespace) + h.except!(:namespace) # TODO: project untested? + _pvc_ref = h.delete(:persistent_volume_claim_ref) + #h[:persistent_volume_claim] = pvc_ref && @data_index.fetch_path( + # path_for_entity("persistent_volume_claim"), + # :by_namespace_and_name, pvc_ref[:namespace], pvc_ref[:name] + # ) collection.build(h) end end @@ -356,17 +381,32 @@ def get_pods_graph(inv) inv["pod"].each do |pod| h = parse_pod(pod) - h.except!(:tags, :namespace) - - h[:container_project] = lazy_find_project(h.delete(:project)) + h.except!(:tags) + h[:container_project] = lazy_find_project(:name => h[:namespace]) + h[:container_node] = lazy_find_node(:name => h.delete(:container_node_name)) + h[:container_replicator] = lazy_find_replicator(h.delete(:container_replicator_ref)) _build_pod_name = h.delete(:build_pod_name) - _custom_attrs = h.extract!(:labels, :node_selector_parts) + custom_attrs = h.extract!(:labels, :node_selector_parts) children = h.extract!(:container_definitions, :containers, :container_conditions, :container_volumes) container_group = collection.build(h) get_container_definitions_graph(container_group, children[:container_definitions]) + get_container_conditions_graph(container_group, children[:container_conditions]) + get_custom_attributes_graph(container_group, + :labels => custom_attrs[:labels], + # The actual section is "node_selectors" + :node_selectors => custom_attrs[:node_selector_parts]) + end + end + + # polymorphic, relation disambiguates parent + def get_container_conditions_graph(parent, hashes) + model_name = parent.inventory_collection.model_class.name + hashes.to_a.each do |h| + h = h.merge(:container_entity => parent) + @inv_collections[[:container_conditions_for, model_name]].build(h) end end @@ -411,29 +451,59 @@ def get_containers_graph(parent, h) collection = @inv_collections[:containers] h[:container_definition] = parent - - _container_image = h.delete(:container_image) + h[:container_image] = lazy_find_image(h[:container_image]) collection.build(h) end - def get_services_graph(inv) + # TODO: how would this work with partial refresh? + # TODO: can I write get_endpoints() that directly refreshes ContainerGroupsContainerServices join table? + def get_endpoints_and_services_graph(inv) + cgs_by_namespace_and_name = {} + + # We don't save endpoints themselves, only parse for cross-linking services<->pods + inv["endpoint"].each do |endpoint| + ep = parse_endpoint(endpoint) + + container_groups = [] + ep.delete(:container_groups_refs).each do |ref| + next if ref.nil? + cg = lazy_find_container_group(:namespace => ref[:namespace], :name => ref[:name]) + container_groups << cg unless cg.nil? + end + cgs_by_namespace_and_name.store_path(ep[:namespace], ep[:name], container_groups) + end + collection = @inv_collections[:container_services] inv["service"].each do |service| h = parse_service(service) - h.except!(:tags, :namespace) + h[:container_project] = lazy_find_project(:name => h[:namespace]) # TODO: untested? - h[:container_project] = lazy_find_project(h.delete(:project)) + custom_attrs = h.extract!(:labels, :selector_parts) + children = h.extract!(:container_service_port_configs) - _custom_attrs = h.extract!(:labels, :selector_parts) - _children = h.extract!(:container_service_port_configs) + _container_image_registry = h.delete(:container_image_registry) # TODO: derive from container_service_port_configs - _container_image_registry = h.delete(:container_image_registry) - _container_groups = h.delete(:container_groups) + h[:container_groups] = cgs_by_namespace_and_name.fetch_path(h[:namespace], h[:name]) - collection.build(h) + h.except!(:tags) + + container_service = collection.build(h) + + get_container_service_port_configs_graph(container_service, children[:container_service_port_configs]) + get_custom_attributes_graph(container_service, + :labels => custom_attrs[:labels], + # The actual section is "selectors" + :selectors => custom_attrs[:selector_parts]) + end + end + + def get_container_service_port_configs_graph(container_service, hashes) + hashes.to_a.each do |h| + h = h.merge(:container_service => container_service) + @inv_collections[:container_service_port_configs].build(h) end end @@ -446,6 +516,43 @@ def get_component_statuses_graph(inv) end end + # TODO: images & registries still rely on @data_index + def get_container_image_registries_graph + collection = @inv_collections[:container_image_registries] + # Resulting from previously parsed images + registries = @data_index.fetch_path(:container_image_registry, :by_host_and_port) || [] + registries.each do |_host_port, ir| + collection.build(ir) + end + end + + def get_container_images_graph + collection = @inv_collections[:container_images] + # Resulting from previously parsed images + images = @data_index.fetch_path(:container_image, :by_digest) || [] + images.each do |_digest, im| + im = im.merge(:container_image_registry => lazy_find_image_registry(im[:container_image_registry])) + custom_attrs = im.extract!(:labels, :docker_labels) + container_image = collection.build(im) + + get_custom_attributes_graph(container_image, custom_attrs) + end + end + + def get_custom_attributes_graph(parent, hashes_by_section) + model_name = parent.inventory_collection.model_class.name + hashes_by_section.each do |section, hashes| + collection = @inv_collections[[:custom_attributes_for, model_name, section.to_s]] + hashes.to_a.each do |h| + h = h.merge(:resource => parent) + if h[:section].to_s != section.to_s + raise("unexpected hash with section #{h[:section]} under #{section}") + end + collection.build(h) + end + end + end + def process_collection(collection, key, &block) @data[key] ||= [] collection.each { |item| process_collection_item(item, key, &block) } @@ -460,6 +567,8 @@ def process_collection_item(item, key) new_result end + ## Shared parsing methods + def map_labels(model_name, labels) ContainerLabelTagMapping.map_labels(@label_tag_mapping, model_name, labels) end @@ -1148,9 +1257,35 @@ def path_for_entity(entity) resource_by_entity(entity).tableize.to_sym end - def lazy_find_project(hash) - return if hash.nil? - @inv_collections[:container_projects].lazy_find(hash[:ems_ref]) + def lazy_find_project(name:) + return nil if name.nil? + @inv_collections[:container_projects].lazy_find(name, :ref => :by_name) + end + + def lazy_find_node(name:) + return nil if name.nil? + @inv_collections[:container_nodes].lazy_find(name, :ref => :by_name) + end + + def lazy_find_replicator(hash) + return nil if hash.nil? + @inv_collections[:container_replicators].lazy_find_by(hash, :ref => :by_namespace_and_name) + end + + def lazy_find_container_group(hash) + return nil if hash.nil? + @inv_collections[:container_groups].lazy_find_by(hash, :ref => :by_namespace_and_name) + end + + def lazy_find_image(hash) + return nil if hash.nil? + hash = hash.merge(:container_image_registry => lazy_find_image_registry(hash[:container_image_registry])) + @inv_collections[:container_images].lazy_find_by(hash) + end + + def lazy_find_image_registry(hash) + return nil if hash.nil? + @inv_collections[:container_image_registries].lazy_find_by(hash) end end end diff --git a/spec/models/manageiq/providers/kubernetes/container_manager/refresher_spec.rb b/spec/models/manageiq/providers/kubernetes/container_manager/refresher_spec.rb index 9edcb428d7..dcf7c947f8 100644 --- a/spec/models/manageiq/providers/kubernetes/container_manager/refresher_spec.rb +++ b/spec/models/manageiq/providers/kubernetes/container_manager/refresher_spec.rb @@ -28,6 +28,8 @@ ) end + let(:check_tag_mapping) { true } + def full_refresh_test 3.times do # Run three times to verify that second & third runs with existing data do not change anything VCR.use_cassette(described_class.name.underscore) do # , :record => :new_episodes) do @@ -59,17 +61,20 @@ def full_refresh_test full_refresh_test end - it "will perform a full refresh with inventory_objects on k8s" do - stub_settings_merge( - :ems_refresh => { - :kubernetes => { - :inventory_object_refresh => true + describe "graph refresh" do + let(:check_tag_mapping) { false } # TODO: pending implementation + + it "will perform a full refresh with inventory_objects on k8s" do + stub_settings_merge( + :ems_refresh => { + :kubernetes => { + :inventory_object_refresh => true + } } - } - ) + ) - pending("full implementation of graph refresh") - full_refresh_test + full_refresh_test + end end def assert_table_counts @@ -169,9 +174,11 @@ def assert_specific_container_group expect(@containergroup.labels).to contain_exactly( label_with_name_value("name", "heapster") ) - expect(@containergroup.tags).to contain_exactly( - tag_in_category_with_description(@name_category, "heapster") - ) + if check_tag_mapping + expect(@containergroup.tags).to contain_exactly( + tag_in_category_with_description(@name_category, "heapster") + ) + end # Check the relation to container node expect(@containergroup.container_node).not_to be_nil @@ -306,9 +313,11 @@ def assert_specific_container_replicator expect(@replicator.labels).to contain_exactly( label_with_name_value("name", "influxGrafana") ) - expect(@replicator.tags).to contain_exactly( - tag_in_category_with_description(@name_category, "influxGrafana") - ) + if check_tag_mapping + expect(@replicator.tags).to contain_exactly( + tag_in_category_with_description(@name_category, "influxGrafana") + ) + end expect(@replicator.selector_parts.count).to eq(1) @group = ContainerGroup.where(:name => "monitoring-influx-grafana-controller-22icy").first