diff --git a/CHANGELOG.md b/CHANGELOG.md index cf2c7a8..a8caa72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ This file is used to list changes made in each version of the chef_auto_accumula ## Unreleased +- Refactor config item matching + ## 0.6.1 - *2024-07-09* - Bump gem dependency versions diff --git a/libraries/chef_auto_accumulator/config/file.rb b/libraries/chef_auto_accumulator/config/file.rb index cd1edc7..5a56d48 100644 --- a/libraries/chef_auto_accumulator/config/file.rb +++ b/libraries/chef_auto_accumulator/config/file.rb @@ -105,23 +105,23 @@ def load_config_file_section_item(config_file) log_chef(:debug) { "Resultant path\n#{debug_var_output(search_object)}" } search_object else - config.select { |cs| match.any? { |mk, mv| kv_test_log(cs, mk, mv) } } + index = config_item_index_match(config, match) + nil_or_empty?(index) ? nil : config.values_at(*index) end log_chef(:debug) { "Filtered items\n#{debug_var_output(item)}" } return if item.nil? - raise unless item.one? || item.empty? - item = item.first + raise "Expected one or no items to be filtered, got #{item.count}" unless item.one? || item.empty? if item log_chef(:info) { "#{config_file} got Match for Filter\n#{debug_var_output(match)}\n\nResult\n\n#{debug_var_output(item)}" } else - log_chef(:info) { "#{config_file} got No Match for Filter\n#{debug_var_output(match)}" } + log_chef(:warn) { "#{config_file} got No Match for Filter\n#{debug_var_output(match)}" } end - item + item.first rescue KeyError nil end @@ -147,9 +147,9 @@ def load_config_file_section_contained_item(config_file) end match = option_config_match - log_chef(:trace) { "Filtering against K/V pairs #{debug_var_output(match)}" } - - item = outer_key_config.filter { |object| match.any? { |k, v| kv_test_log(object, k, v) } } + log_chef(:debug) { "Filtering against K/V pairs #{debug_var_output(match)}" } + index = config_item_index_match(outer_key_config, match) + item = nil_or_empty?(index) ? nil : outer_key_config.values_at(*index) if nil_or_empty?(item) log_chef(:info) { "#{config_file} got No Match for Filter\n#{debug_var_output(match)}" } @@ -184,6 +184,33 @@ def config_file_config_present? !config.nil? end + # Match a configuration item against multiple conditions, return the highest matching element + # + # @param config [Array] The configuration set to search + # @param match [Hash] The match criteria + # @return [Any] Matched result + # + def config_item_index_match(config, match) + return if nil_or_empty?(config) + raise FileConfigPathFilterError, 'Empty resource match filter set' if nil_or_empty?(match) + + # Find the config items that match at least one of the match filters, then sort to bring the high matches to the top + matched_items = config.each_with_index.map { |c, i| [i, match.map { |k, v| kv_test_log(c, k, v) }.count(true)] }.filter { |_, c| c.positive? } + matched_items.sort_by! { |_, count| -count } + + return unless matched_items.count.positive? + + # Get the number of matches for the 'best' match then find out how many items matched at this level + best_match = matched_items.first.last + index = matched_items.filter { |_, c| c.eql?(best_match) }.map(&:first) + + unless index.one? + log_chef(:warn) { "Expected either one or zero filtered configuration items, got #{index.count}. Data:\n#{debug_var_output(index)}" } + end + + index + end + # Error to raise when failing to filter a single containing resource from a parent path class FileConfigPathFilterError < FilterError; end end diff --git a/libraries/chef_auto_accumulator/resource.rb b/libraries/chef_auto_accumulator/resource.rb index 5d44bb7..ffaebb9 100644 --- a/libraries/chef_auto_accumulator/resource.rb +++ b/libraries/chef_auto_accumulator/resource.rb @@ -255,27 +255,29 @@ def accumulator_config_array_index match = option_config_match # Find the Array index for the configuration object that matches the resource definition - index = case option_config_path_type - when :array - log_chef(:debug) { "Testing :array for #{debug_var_output(match)}" } - - array_path = accumulator_config_path_init(action, *path) - array_path.each_with_index.select { |obj, _| match.any? { |k, v| kv_test_log(obj, k, v) } }.map(&:last) - when :array_contained - ck = accumulator_config_path_containing_key - - log_chef(:debug) { "Searching :array_contained #{debug_var_output(ck)} against #{debug_var_output(match)}" } - - array_cpath = accumulator_config_containing_path_init(action: action, path: path) - return unless array_cpath - - # Fetch the containing key and filter for any objects that match the filter - array_cpath.fetch(ck, []).each_with_index.select { |obj, _| match.any? { |k, v| kv_test_log(obj, k, v) } }.map(&:last) - else - raise ArgumentError "Unknown config path type #{debug_var_output(option_config_path_type)}" - end - - index.reverse! # We need the indexes in reverse order so we delete correctly, otherwise the shift will result in left over objects we intended to delete + array_path = case option_config_path_type + when :array + log_chef(:debug) { "Testing :array for #{debug_var_output(match)}" } + accumulator_config_path_init(action, *path) + when :array_contained + ck = accumulator_config_path_containing_key + + log_chef(:debug) { "Searching :array_contained #{debug_var_output(ck)} against #{debug_var_output(match)}" } + + array_path = accumulator_config_containing_path_init(action: action, path: path) + return unless array_path + + # Fetch the containing key and filter for any objects that match the filter + array_path = array_path.fetch(ck, []) + log_chef(:debug) { "Path: #{debug_var_output(array_path)}" } + array_path + else + raise ArgumentError "Unknown config path type #{debug_var_output(option_config_path_type)}" + end + + index = config_item_index_match(array_path, match) + # We need the indexes in reverse order so we delete correctly, otherwise the shift will result in left over objects we intended to delete + index.reverse! unless nil_or_empty?(index) log_chef(:debug) { "Result #{debug_var_output(index)}" } index