From d3250538a9e3fe299becbbe04d8d79bb0cb6d05c Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Tue, 30 Jan 2024 12:50:01 +0100 Subject: [PATCH] Allow lookup of hiera keys #264 --- .rubocop_todo.yml | 5 - Gemfile | 1 + Gemfile.lock | 1 + app/models/hiera_data.rb | 45 +++++- app/models/hiera_data/data_file.rb | 3 +- app/models/hiera_data/lookup.rb | 60 ++++++++ app/models/key.rb | 17 +-- .../lookup_tests/data/common.yaml | 29 ++++ .../data/nodes/lookup.betadots.training.yaml | 16 ++ .../lookup_tests/data/role/test_role.yaml | 18 +++ .../lookup_tests/data/zone/test_zone.yaml | 15 ++ .../environments/lookup_tests/hiera.yaml | 16 ++ .../nodes/lookup.betadots.training_facts.yaml | 5 + test/models/environment_test.rb | 1 + test/models/hiera_data/config_test.rb | 103 ++++++------- test/models/hiera_data/data_file_test.rb | 82 +++++----- test/models/hiera_data/git_repo_test.rb | 18 ++- test/models/hiera_data/interpolation_test.rb | 130 ++++++++-------- test/models/hiera_data/lookup_test.rb | 140 ++++++++++++++++++ test/models/hiera_data/yaml_file_test.rb | 6 +- test/models/hiera_data_test.rb | 47 ++++-- test/models/key_test.rb | 40 ----- 22 files changed, 549 insertions(+), 249 deletions(-) create mode 100644 app/models/hiera_data/lookup.rb create mode 100644 test/fixtures/files/puppet/environments/lookup_tests/data/common.yaml create mode 100644 test/fixtures/files/puppet/environments/lookup_tests/data/nodes/lookup.betadots.training.yaml create mode 100644 test/fixtures/files/puppet/environments/lookup_tests/data/role/test_role.yaml create mode 100644 test/fixtures/files/puppet/environments/lookup_tests/data/zone/test_zone.yaml create mode 100644 test/fixtures/files/puppet/environments/lookup_tests/hiera.yaml create mode 100644 test/fixtures/files/puppet/nodes/lookup.betadots.training_facts.yaml create mode 100644 test/models/hiera_data/lookup_test.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 385cc705..fc68f99d 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -316,11 +316,6 @@ Style/BlockDelimiters: Style/ClassAndModuleChildren: Exclude: - 'test/channels/application_cable/connection_test.rb' - - 'test/models/hiera_data/config_test.rb' - - 'test/models/hiera_data/data_file_test.rb' - - 'test/models/hiera_data/git_repo_test.rb' - - 'test/models/hiera_data/interpolation_test.rb' - - 'test/models/hiera_data/yaml_file_test.rb' # Offense count: 1 # This cop supports safe auto-correction (--auto-correct). diff --git a/Gemfile b/Gemfile index 47adb424..7f497e85 100644 --- a/Gemfile +++ b/Gemfile @@ -35,6 +35,7 @@ gem 'breadcrumbs_on_rails' gem 'cancancan' gem 'ruby-saml' gem 'diffy' +gem 'deep_merge', require: "deep_merge/core" # To use retry middleware with Faraday v2.0+ gem 'faraday-retry' diff --git a/Gemfile.lock b/Gemfile.lock index 4a7ca46d..42db82ac 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -483,6 +483,7 @@ DEPENDENCIES capybara (>= 2.15) dartsass-rails dartsass-sprockets + deep_merge diffy factory_bot_rails faker diff --git a/app/models/hiera_data.rb b/app/models/hiera_data.rb index 82e255ea..dd1945ef 100644 --- a/app/models/hiera_data.rb +++ b/app/models/hiera_data.rb @@ -124,15 +124,23 @@ def encrypt_value(hierarchy_name, value) token.to_encrypted format: :string end - def lookup_options(facts) - result = {} - config.hierarchies.each do |hierarchy| - hierarchy.resolved_paths(facts:).each do |path| - file = DataFile.new(path: hierarchy.datadir(facts:).join(path)) - result = (file["lookup_options"] || {}).merge(result) - end + def lookup_options_for(key, facts: {}) + candidates = lookup_for(facts) + .lookup("lookup_options", merge_strategy: :hash) + merge = extract_merge_value(key, candidates) + case merge + when String + merge + when Hash + merge["strategy"] + else + "first" end - result + end + + def lookup(key, facts: {}) + merge_strategy = lookup_options_for(key, facts:).to_sym + lookup_for(facts).lookup(key, merge_strategy:) end private @@ -145,5 +153,26 @@ def config def find_hierarchy(name) config.hierarchies.find { |h| h.name == name } end + + def lookup_for(facts) + @cached_lookups ||= {} + @cached_lookups[facts] ||= begin + hashes = config.hierarchies.flat_map do |hierarchy| + hierarchy.resolved_paths(facts:).map do |path| + DataFile.new(path: hierarchy.datadir(facts:).join(path)) + .content + end + end + Lookup.new(hashes.compact) + end + end + + def extract_merge_value(key, candidates) + result = candidates&.dig(key) + result ||= candidates&.select { |k, _v| k[0] == "^" } + &.find { |k, _v| key.match(Regexp.new(k)) } + &.last + result&.dig("merge") + end end # rubocop:enable Metrics/ClassLength diff --git a/app/models/hiera_data/data_file.rb b/app/models/hiera_data/data_file.rb index 8f341661..fcae9c2c 100644 --- a/app/models/hiera_data/data_file.rb +++ b/app/models/hiera_data/data_file.rb @@ -2,7 +2,8 @@ class HieraData class DataFile attr_reader :path, :file - delegate :exist?, :writable?, :keys, :content_for_key, :[], + delegate :content, :exist?, :writable?, :keys, + :content_for_key, :[], to: :file def initialize(path:, facts: {}, type: :yaml) diff --git a/app/models/hiera_data/lookup.rb b/app/models/hiera_data/lookup.rb new file mode 100644 index 00000000..6be870ef --- /dev/null +++ b/app/models/hiera_data/lookup.rb @@ -0,0 +1,60 @@ +class HieraData + class Lookup + def initialize(hashes) + @hashes = hashes + end + + def lookup(key, merge_strategy: :first) + applicable_values = extract_key_from_hashes(key) + merge_values(applicable_values, merge_strategy) + end + + private + + def extract_key_from_hashes(key) + @hashes + .select { |h| h.key?(key) } + .pluck(key) + end + + def merge_values(values, strategy) + case strategy + when :first + values.first + when :unique + unique_array_merge(values) + when :hash + flat_hash_merge(values) + when :deep + deep_merge(values) + end + end + + def unique_array_merge(values) + values.inject do |memo, value| + memo ||= [] + raise Hdm::Error, "Merge strategy `unique` is not applicable to a hash." if value.is_a?(Hash) + + memo + Array(value) + end.uniq + end + + def flat_hash_merge(values) + values.inject do |memo, value| + memo ||= {} + raise Hdm::Error, "Merge strategy `hash` can only be used with hashes." unless value.is_a?(Hash) + + value.merge(memo) + end + end + + def deep_merge(values) + values.inject do |memo, value| + memo ||= {} + raise Hdm::Error, "Merge strategy `deep` can only be used with hashes and arrays." unless value.is_a?(Hash) || value.is_a?(Array) + + DeepMerge.deep_merge!(memo, value, merge_hash_arrays: true) + end + end + end +end diff --git a/app/models/key.rb b/app/models/key.rb index f50f1e8e..d7c222d5 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -39,21 +39,6 @@ def to_s private def load_lookup_options(node) - lookup_option_candidates = hiera_data.lookup_options(node.facts) - result = lookup_option_candidates.find do |key, options| - key == name - end - result ||= lookup_option_candidates.find do |key, options| - name.match(Regexp.new("\\A#{key}\\z")) - end - merge = result&.last&.dig("merge") - case merge - when String - merge - when Hash - merge["stategy"] - else - "first" - end + hiera_data.lookup_options_for(name, facts: node.facts) end end diff --git a/test/fixtures/files/puppet/environments/lookup_tests/data/common.yaml b/test/fixtures/files/puppet/environments/lookup_tests/data/common.yaml new file mode 100644 index 00000000..636779ce --- /dev/null +++ b/test/fixtures/files/puppet/environments/lookup_tests/data/common.yaml @@ -0,0 +1,29 @@ +--- +hdm_integer: 93 +hdm_unique_array: + - common +hdm_duplicates_in_array: + - node + - zone + - common +hdm_nested_array: + - from_common + - - nested + - list +hdm_simple_hash: + origin: common + added_by_common: common +hdm_nested_hash: + origin: common + nested_hash: + integer: 32 + added_by_common: common +lookup_options: + '^hdm_duplicates': + merge: hash + hdm_duplicates_in_array: + merge: unique + hash_syntax: + merge: + strategy: deep + diff --git a/test/fixtures/files/puppet/environments/lookup_tests/data/nodes/lookup.betadots.training.yaml b/test/fixtures/files/puppet/environments/lookup_tests/data/nodes/lookup.betadots.training.yaml new file mode 100644 index 00000000..af1d6d20 --- /dev/null +++ b/test/fixtures/files/puppet/environments/lookup_tests/data/nodes/lookup.betadots.training.yaml @@ -0,0 +1,16 @@ +--- +hdm_integer: 23 +hdm_unique_array: + - node +hdm_duplicates_in_array: + - node +hdm_nested_array: + - duplicate + - nested_hash: + node_integer: 2 +hdm_simple_hash: + origin: node +hdm_nested_hash: + origin: node + nested_hash: + integer: 5 diff --git a/test/fixtures/files/puppet/environments/lookup_tests/data/role/test_role.yaml b/test/fixtures/files/puppet/environments/lookup_tests/data/role/test_role.yaml new file mode 100644 index 00000000..71aae33a --- /dev/null +++ b/test/fixtures/files/puppet/environments/lookup_tests/data/role/test_role.yaml @@ -0,0 +1,18 @@ +--- +hdm_integer: 42 +hdm_string: "role" +hdm_unique_array: + - role +hdm_duplicates_in_array: + - node + - role +hdm_nested_array: + - from_role + - nested_hash: + role_integer: 2 +hdm_simple_hash: + origin: role + added_by_role: role +hdm_nested_hash: + nested_hash: + added_by_role: role diff --git a/test/fixtures/files/puppet/environments/lookup_tests/data/zone/test_zone.yaml b/test/fixtures/files/puppet/environments/lookup_tests/data/zone/test_zone.yaml new file mode 100644 index 00000000..aaa6f279 --- /dev/null +++ b/test/fixtures/files/puppet/environments/lookup_tests/data/zone/test_zone.yaml @@ -0,0 +1,15 @@ +--- +hdm_unique_array: + - zone +hdm_duplicates_in_array: + - node + - zone +hdm_nested_array: + - duplicate +hdm_simple_hash: + added_by_zone: zone +hdm_nested_hash: + origin: zone + nested_hash: + integer: 44 + added_by_zone: zone diff --git a/test/fixtures/files/puppet/environments/lookup_tests/hiera.yaml b/test/fixtures/files/puppet/environments/lookup_tests/hiera.yaml new file mode 100644 index 00000000..30a2343f --- /dev/null +++ b/test/fixtures/files/puppet/environments/lookup_tests/hiera.yaml @@ -0,0 +1,16 @@ +version: 5 +defaults: + datadir: data + data_hash: yaml_data + +hierarchy: + - name: "Host specific" + path: "nodes/%{::facts.fqdn}.yaml" + + - name: "Role / zone specific" # Uses custom facts. + paths: + - "role/%{::facts.role}.yaml" + - "zone/%{::facts.zone}.yaml" + + - name: "Global data" + path: "common.yaml" diff --git a/test/fixtures/files/puppet/nodes/lookup.betadots.training_facts.yaml b/test/fixtures/files/puppet/nodes/lookup.betadots.training_facts.yaml new file mode 100644 index 00000000..75d355b9 --- /dev/null +++ b/test/fixtures/files/puppet/nodes/lookup.betadots.training_facts.yaml @@ -0,0 +1,5 @@ +--- +fqdn: 'lookup.betadots.training' +role: 'test_role' +environment: 'lookup_tests' +zone: 'test_zone' diff --git a/test/models/environment_test.rb b/test/models/environment_test.rb index f5372c78..fb4c2387 100644 --- a/test/models/environment_test.rb +++ b/test/models/environment_test.rb @@ -9,6 +9,7 @@ class EnvironmentTest < ActiveSupport::TestCase eyaml globs hdm + lookup_tests minimal multiple_hierarchies no_config diff --git a/test/models/hiera_data/config_test.rb b/test/models/hiera_data/config_test.rb index 725fcb3a..76f2db71 100644 --- a/test/models/hiera_data/config_test.rb +++ b/test/models/hiera_data/config_test.rb @@ -1,71 +1,74 @@ require 'test_helper' -class HieraData::ConfigTest < ActiveSupport::TestCase - class HieraData::ConfigV3 < ActiveSupport::TestCase - test "does not support v3 config style" do - assert_raise Hdm::Error do - HieraData::Config.new(base_path) +class HieraData + class ConfigTest < ActiveSupport::TestCase + class ConfigV3 < ActiveSupport::TestCase + test "does not support v3 config style" do + assert_raise Hdm::Error do + HieraData::Config.new(base_path) + end end - end - def base_path - Pathname.new(Rails.configuration.hdm["config_dir"]).join("environments", "v3") - end - end - class HieraData::ConfigNoYamlFilePresent < ActiveSupport::TestCase - test "uses defaults from puppet" do - config = HieraData::Config.new(base_path) - assert_equal 1, config.hierarchies.size + def base_path + Pathname.new(Rails.configuration.hdm["config_dir"]).join("environments", "v3") + end end - def base_path - Pathname.new(Rails.configuration.hdm["config_dir"]).join("environments", "no_config") - end - end + class ConfigNoYamlFilePresent < ActiveSupport::TestCase + test "uses defaults from puppet" do + config = HieraData::Config.new(base_path) + assert_equal 1, config.hierarchies.size + end - class HieraData::ConfigMinimalIncompleteYamlFile < ActiveSupport::TestCase - test "merges with defaults from puppet" do - config = HieraData::Config.new(base_path) - assert_equal 1, config.hierarchies.size + def base_path + Pathname.new(Rails.configuration.hdm["config_dir"]).join("environments", "no_config") + end end - def base_path - Pathname.new(Rails.configuration.hdm["config_dir"]).join("environments", "minimal") - end - end + class ConfigMinimalIncompleteYamlFile < ActiveSupport::TestCase + test "merges with defaults from puppet" do + config = HieraData::Config.new(base_path) + assert_equal 1, config.hierarchies.size + end - class HieraData::ConfigNoDatadirInYamlFile < ActiveSupport::TestCase - test "uses default datadir from puppet" do - config = HieraData::Config.new(base_path) - assert_not_nil config.content["defaults"] - assert_equal Puppet::Pops::Lookup::HieraConfigV5::DEFAULT_CONFIG_HASH["defaults"]["datadir"], config.content["defaults"]["datadir"] + def base_path + Pathname.new(Rails.configuration.hdm["config_dir"]).join("environments", "minimal") + end end - def base_path - Pathname.new(Rails.configuration.hdm["config_dir"]).join("environments", "no_datadir") - end - end + class ConfigNoDatadirInYamlFile < ActiveSupport::TestCase + test "uses default datadir from puppet" do + config = HieraData::Config.new(base_path) + assert_not_nil config.content["defaults"] + assert_equal Puppet::Pops::Lookup::HieraConfigV5::DEFAULT_CONFIG_HASH["defaults"]["datadir"], config.content["defaults"]["datadir"] + end - class HieraData::ConfigWithSomeHierarchiesTest < ActiveSupport::TestCase - test "when only defaults, return the yaml paths" do - config = HieraData::Config.new(base_path) - assert_equal 3, config.hierarchies.size + def base_path + Pathname.new(Rails.configuration.hdm["config_dir"]).join("environments", "no_datadir") + end end - def base_path - Pathname.new(Rails.configuration.hdm["config_dir"]).join("environments", "multiple_hierarchies") - end - end + class ConfigWithSomeHierarchiesTest < ActiveSupport::TestCase + test "when only defaults, return the yaml paths" do + config = HieraData::Config.new(base_path) + assert_equal 3, config.hierarchies.size + end - class HieraData::ConfigWithEmptyDefaultsTest < ActiveSupport::TestCase - test "empty defaults get replaced" do - config = HieraData::Config.new(base_path) - assert_not_nil config.content["defaults"] - assert_equal Puppet::Pops::Lookup::HieraConfigV5::DEFAULT_CONFIG_HASH["defaults"], config.content["defaults"] + def base_path + Pathname.new(Rails.configuration.hdm["config_dir"]).join("environments", "multiple_hierarchies") + end end - def base_path - Pathname.new(Rails.configuration.hdm["config_dir"]).join("environments", "empty_defaults") + class ConfigWithEmptyDefaultsTest < ActiveSupport::TestCase + test "empty defaults get replaced" do + config = HieraData::Config.new(base_path) + assert_not_nil config.content["defaults"] + assert_equal Puppet::Pops::Lookup::HieraConfigV5::DEFAULT_CONFIG_HASH["defaults"], config.content["defaults"] + end + + def base_path + Pathname.new(Rails.configuration.hdm["config_dir"]).join("environments", "empty_defaults") + end end end end diff --git a/test/models/hiera_data/data_file_test.rb b/test/models/hiera_data/data_file_test.rb index 5d261ced..97e05047 100644 --- a/test/models/hiera_data/data_file_test.rb +++ b/test/models/hiera_data/data_file_test.rb @@ -1,53 +1,55 @@ require 'test_helper' -class HieraData::DataFileTest < ActiveSupport::TestCase - class GitIntegration < ActiveSupport::TestCase - DATADIR = Rails.root.join('test', 'fixtures', 'files', 'puppet', 'environments', 'development', 'data') - PATH = DATADIR.join('nodes', 'writehost.yaml') +class HieraData + class DataFileTest < ActiveSupport::TestCase + class GitIntegration < ActiveSupport::TestCase + DATADIR = Rails.root.join('test', 'fixtures', 'files', 'puppet', 'environments', 'development', 'data') + PATH = DATADIR.join('nodes', 'writehost.yaml') - setup do - Rails.configuration.hdm["git_data"] = [ - { - datadir: DATADIR.to_s, - path_in_repo: "/", - git_url: "git@example.com/example.git" - } - ] - end + setup do + Rails.configuration.hdm["git_data"] = [ + { + datadir: DATADIR.to_s, + path_in_repo: "/", + git_url: "git@example.com/example.git" + } + ] + end - teardown do - Rails.configuration.hdm["git_data"] = nil - end + teardown do + Rails.configuration.hdm["git_data"] = nil + end - test "#write_key triggers git commit" do - git_repo = Minitest::Mock.new - git_repo.expect(:local_path, DATADIR) - git_repo.expect(:commit!, true, [:add, PATH]) - HieraData::GitRepo.stub(:new, git_repo) do - with_temp_file(PATH) do - expected_hash = {"test_key"=>"true"} - file = HieraData::DataFile.new(path: PATH) - file.write_key('test_key', 'true') - assert_equal expected_hash, YAML.load_file(PATH) + test "#write_key triggers git commit" do + git_repo = Minitest::Mock.new + git_repo.expect(:local_path, DATADIR) + git_repo.expect(:commit!, true, [:add, PATH]) + HieraData::GitRepo.stub(:new, git_repo) do + with_temp_file(PATH) do + expected_hash = {"test_key"=>"true"} + file = HieraData::DataFile.new(path: PATH) + file.write_key('test_key', 'true') + assert_equal expected_hash, YAML.load_file(PATH) + end end + git_repo.verify end - git_repo.verify - end - test "#remove_key triggers git commit" do - git_repo = Minitest::Mock.new - git_repo.expect(:local_path, DATADIR) - git_repo.expect(:commit!, true, [:remove, PATH]) - HieraData::GitRepo.stub(:new, git_repo) do - with_temp_file(PATH) do - File.write(PATH, {"test_key" => true}.to_yaml) - expected_hash = {} - file = HieraData::DataFile.new(path: PATH) - file.remove_key('test_key') - assert_equal expected_hash, YAML.load_file(PATH) + test "#remove_key triggers git commit" do + git_repo = Minitest::Mock.new + git_repo.expect(:local_path, DATADIR) + git_repo.expect(:commit!, true, [:remove, PATH]) + HieraData::GitRepo.stub(:new, git_repo) do + with_temp_file(PATH) do + File.write(PATH, {"test_key" => true}.to_yaml) + expected_hash = {} + file = HieraData::DataFile.new(path: PATH) + file.remove_key('test_key') + assert_equal expected_hash, YAML.load_file(PATH) + end end + git_repo.verify end - git_repo.verify end end end diff --git a/test/models/hiera_data/git_repo_test.rb b/test/models/hiera_data/git_repo_test.rb index 1b884c3e..314f93c4 100644 --- a/test/models/hiera_data/git_repo_test.rb +++ b/test/models/hiera_data/git_repo_test.rb @@ -1,15 +1,17 @@ require 'test_helper' -class HieraData::GitRepoTest < ActiveSupport::TestCase - test "#initialize raises when given invalid git uri" do - assert_raises(Hdm::Error) do - HieraData::GitRepo.new("git@://invalid_uri") +class HieraData + class GitRepoTest < ActiveSupport::TestCase + test "#initialize raises when given invalid git uri" do + assert_raises(Hdm::Error) do + HieraData::GitRepo.new("git@://invalid_uri") + end end - end - test "#intialize raises when given a non-existant repo" do - assert_raises(Hdm::Error) do - HieraData::GitRepo.new("git@example.invalid:repost/test.git") + test "#intialize raises when given a non-existant repo" do + assert_raises(Hdm::Error) do + HieraData::GitRepo.new("git@example.invalid:repost/test.git") + end end end end diff --git a/test/models/hiera_data/interpolation_test.rb b/test/models/hiera_data/interpolation_test.rb index 38431751..1307b5a5 100644 --- a/test/models/hiera_data/interpolation_test.rb +++ b/test/models/hiera_data/interpolation_test.rb @@ -1,68 +1,70 @@ require 'test_helper' -class HieraData::InterpolationTest < ActiveSupport::TestCase - test "::interpolate_globs handles globs" do - datadir = Rails.root.join("test/fixtures/files/puppet/environments/globs/data") - path = "common/*.yaml" - expected_result = [ - "common/foobar.yaml", - "common/hdm.yaml" - ] - - assert_equal expected_result, HieraData::Interpolation.interpolate_globs(path:, datadir:) - end - - test "::interpolate_facts replaces facts with leading colons" do - path = "/test/%{::facts.test_fact}/file" - facts = {"test_fact" => "replaced"} - expected_result = "/test/replaced/file" - - assert_equal expected_result, HieraData::Interpolation.interpolate_facts(path:, facts:) - end - - test "::interpolate_facts replaces facts without leading colons" do - path = "/test/%{facts.test_fact}/file" - facts = {"test_fact" => "replaced"} - expected_result = "/test/replaced/file" - - assert_equal expected_result, HieraData::Interpolation.interpolate_facts(path:, facts:) - end - - test "::interpolate_facts replaces facts with `trusted.` scope with leading colons" do - path = "/test/%{::trusted.test_fact}/file" - facts = {"trusted" => {"test_fact" => "replaced"}} - expected_result = "/test/replaced/file" - - assert_equal expected_result, HieraData::Interpolation.interpolate_facts(path:, facts:) - end - - test "::interpolate_facts replaces facts with `trusted.` scope without leading colons" do - path = "/test/%{trusted.test_fact}/file" - facts = {"trusted" => {"test_fact" => "replaced"}} - expected_result = "/test/replaced/file" - - assert_equal expected_result, HieraData::Interpolation.interpolate_facts(path:, facts:) - end - - test "::interpolate_facts replaces nested facts" do - path = "/test/%{facts.nested.fact}/file" - facts = {"nested" => {"fact" => "deep"}} - expected_result = "/test/deep/file" - - assert_equal expected_result, HieraData::Interpolation.interpolate_facts(path:, facts:) - end - - test "::interpolate_facts will not replace arbitrary variables with leading colons" do - path = "/test/%{::test_fact}/file" - facts = {"test_fact" => "replaced"} - - assert_equal path, HieraData::Interpolation.interpolate_facts(path:, facts:) - end - - test "::interpolate_facts will not replace arbitrary variables without leading colons" do - path = "/test/%{test_fact}/file" - facts = {"test_fact" => "replaced"} - - assert_equal path, HieraData::Interpolation.interpolate_facts(path:, facts:) +class HieraData + class InterpolationTest < ActiveSupport::TestCase + test "::interpolate_globs handles globs" do + datadir = Rails.root.join("test/fixtures/files/puppet/environments/globs/data") + path = "common/*.yaml" + expected_result = [ + "common/foobar.yaml", + "common/hdm.yaml" + ] + + assert_equal expected_result, HieraData::Interpolation.interpolate_globs(path:, datadir:) + end + + test "::interpolate_facts replaces facts with leading colons" do + path = "/test/%{::facts.test_fact}/file" + facts = {"test_fact" => "replaced"} + expected_result = "/test/replaced/file" + + assert_equal expected_result, HieraData::Interpolation.interpolate_facts(path:, facts:) + end + + test "::interpolate_facts replaces facts without leading colons" do + path = "/test/%{facts.test_fact}/file" + facts = {"test_fact" => "replaced"} + expected_result = "/test/replaced/file" + + assert_equal expected_result, HieraData::Interpolation.interpolate_facts(path:, facts:) + end + + test "::interpolate_facts replaces facts with `trusted.` scope with leading colons" do + path = "/test/%{::trusted.test_fact}/file" + facts = {"trusted" => {"test_fact" => "replaced"}} + expected_result = "/test/replaced/file" + + assert_equal expected_result, HieraData::Interpolation.interpolate_facts(path:, facts:) + end + + test "::interpolate_facts replaces facts with `trusted.` scope without leading colons" do + path = "/test/%{trusted.test_fact}/file" + facts = {"trusted" => {"test_fact" => "replaced"}} + expected_result = "/test/replaced/file" + + assert_equal expected_result, HieraData::Interpolation.interpolate_facts(path:, facts:) + end + + test "::interpolate_facts replaces nested facts" do + path = "/test/%{facts.nested.fact}/file" + facts = {"nested" => {"fact" => "deep"}} + expected_result = "/test/deep/file" + + assert_equal expected_result, HieraData::Interpolation.interpolate_facts(path:, facts:) + end + + test "::interpolate_facts will not replace arbitrary variables with leading colons" do + path = "/test/%{::test_fact}/file" + facts = {"test_fact" => "replaced"} + + assert_equal path, HieraData::Interpolation.interpolate_facts(path:, facts:) + end + + test "::interpolate_facts will not replace arbitrary variables without leading colons" do + path = "/test/%{test_fact}/file" + facts = {"test_fact" => "replaced"} + + assert_equal path, HieraData::Interpolation.interpolate_facts(path:, facts:) + end end end diff --git a/test/models/hiera_data/lookup_test.rb b/test/models/hiera_data/lookup_test.rb new file mode 100644 index 00000000..ca3377f1 --- /dev/null +++ b/test/models/hiera_data/lookup_test.rb @@ -0,0 +1,140 @@ +require 'test_helper' + +class HieraData + class LookupTest < ActiveSupport::TestCase + setup do + @lookup = HieraData::Lookup.new(hashes) + end + + test "looking up simple integer with merge strategy `first`" do + result = perform_lookup("hdm_integer", merge_strategy: :first) + assert_equal 23, result + end + + test "looking up simple integer with merge strategy `hash`" do + assert_raises Hdm::Error do + perform_lookup("hdm_integer", merge_strategy: :hash) + end + end + + test "looking up a simple array with merge strategy `first`" do + result = perform_lookup("hdm_unique_array", merge_strategy: :first) + assert_equal ["node"], result + end + + test "looking up a simple array with merge strategy `unique`" do + result = perform_lookup("hdm_unique_array", merge_strategy: :unique) + assert_equal %w[node role zone common], result + end + + test "looking up an array with duplicates with merge strategy `unique`" do + result = perform_lookup("hdm_duplicates_in_array", merge_strategy: :unique) + assert_equal %w[node role zone common], result + end + + test "looking up a complex nested array with merge strategy `unique`" do + result = perform_lookup("hdm_nested_array", merge_strategy: :unique) + expected = [ + "duplicate", + { "nested_hash" => { + "node_integer" => 2 + } }, + "from_role", + { "nested_hash" => { + "role_integer" => 2 + } }, + "from_common", + ["nested", "list"] + ] + assert_equal expected, result + end + + test "looking up a complex nested array with merge strategy `deep`" do + result = perform_lookup("hdm_nested_array", merge_strategy: :deep) + expected = [ + "from_common", + ["nested", "list"], + "duplicate", + "from_role", + { "nested_hash" => { + "role_integer" => 2 + } }, + { "nested_hash" => { + "node_integer" => 2 + } } + ] + assert_equal expected, result + end + + test "looking up a simple hash with merge strategy `first`" do + result = perform_lookup("hdm_simple_hash", merge_strategy: :first) + assert_equal({ "origin" => "node" }, result) + end + + test "looking up a simple hash with merge strategy `hash`" do + result = perform_lookup("hdm_simple_hash", merge_strategy: :hash) + expected = { + "added_by_common" => "common", + "added_by_zone" => "zone", + "added_by_role" => "role", + "origin" => "node" + } + assert_equal expected, result + end + + test "looking up a simple hash with merge strategy `deep`" do + result = perform_lookup("hdm_simple_hash", merge_strategy: :deep) + expected = { + "added_by_common" => "common", + "added_by_zone" => "zone", + "added_by_role" => "role", + "origin" => "node" + } + assert_equal expected, result + end + + test "looking up a nested hash with merge strategy `hash`" do + result = perform_lookup("hdm_nested_hash", merge_strategy: :hash) + expected = { + "origin" => "node", + "nested_hash" => { + "integer" => 5 + } + } + assert_equal expected, result + end + + test "looking up a nested hash with merge strategy `deep`" do + result = perform_lookup("hdm_nested_hash", merge_strategy: :deep) + expected = { + "origin" => "node", + "nested_hash" => { + "integer" => 5, + "added_by_common" => "common", + "added_by_zone" => "zone", + "added_by_role" => "role" + } + } + assert_equal expected, result + end + + private + + def perform_lookup(key, merge_strategy:) + @lookup.lookup(key, merge_strategy:) + end + + def hashes + @hashes ||= [ + "nodes/lookup.betadots.training.yaml", + "role/test_role.yaml", + "zone/test_zone.yaml", + "common.yaml" + ].map { |f| YAML.load_file(base_path.join(f)) } + end + + def base_path + Rails.root.join("test/fixtures/files/puppet/environments/lookup_tests/data") + end + end +end diff --git a/test/models/hiera_data/yaml_file_test.rb b/test/models/hiera_data/yaml_file_test.rb index 05634675..8b60ffca 100644 --- a/test/models/hiera_data/yaml_file_test.rb +++ b/test/models/hiera_data/yaml_file_test.rb @@ -1,6 +1,7 @@ require 'test_helper' -class HieraData::YamlFileTest < ActiveSupport::TestCase +class HieraData + class YamlFileTest < ActiveSupport::TestCase test "#exist? return false for non existing file" do file = HieraData::YamlFile.new(path: config_dir.join("role/hdm_test-development.yaml")) refute file.exist? @@ -124,7 +125,7 @@ class HieraData::YamlFileTest < ActiveSupport::TestCase end end - private + private def config_dir Pathname.new(Rails.configuration.hdm["config_dir"]).join('environments', 'development', 'data') end @@ -136,4 +137,5 @@ def key_as_string template: foobar/postfix/main.cf.epp HEREDOC end + end end diff --git a/test/models/hiera_data_test.rb b/test/models/hiera_data_test.rb index d1e7c602..74384bdb 100644 --- a/test/models/hiera_data_test.rb +++ b/test/models/hiera_data_test.rb @@ -85,21 +85,6 @@ class HieraDataTest < ActiveSupport::TestCase assert_no_match /top secret/, ciphertext end - test "#lookup_options returns the merged lookup options for the selected environment and facts" do - hiera = HieraData.new("multiple_hierarchies") - node = Node.new(hostname: "60wxmaw5.betadots.training", environment: "multiple_hierarchies") - expected_hash = { - 'profile::auth::sudo_configs' => { - "merge" => "deep" - }, - 'profile::auth::sshd_config_allowgroups' => { - "merge" => "first" - } - } - - assert_equal expected_hash, hiera.lookup_options(node.facts) - end - test "#files_including returns file information for a given key" do hiera = HieraData.new("multiple_hierarchies") expected_result = [ @@ -119,4 +104,36 @@ class HieraDataTest < ActiveSupport::TestCase assert_equal expected_result, hiera.files_including("foobar::timezone") end + + test "#lookup returns the correct result for the given environment and facts" do + hiera = HieraData.new("lookup_tests") + node = Node.new(hostname: "lookup.betadots.training", environment: "lookup_tests") + expected = %w[node role zone common] + + assert_equal expected, hiera.lookup("hdm_duplicates_in_array", facts: node.facts) + end + + test "#lookup_options_for can use hash syntax" do + hiera = HieraData.new("lookup_tests") + + assert_equal "deep", hiera.lookup_options_for("hash_syntax") + end + + test "#lookup_options_for favors literal match over regexp" do + hiera = HieraData.new("lookup_tests") + + assert_equal "unique", hiera.lookup_options_for("hdm_duplicates_in_array") + end + + test "#lookup_options_for uses first regexp match if no literal match possible" do + hiera = HieraData.new("lookup_tests") + + assert_equal "hash", hiera.lookup_options_for("hdm_duplicates_hash") + end + + test "#lookup_options_for defaults to first" do + hiera = HieraData.new("lookup_tests") + + assert_equal "first", hiera.lookup_options_for("hdm_integer") + end end diff --git a/test/models/key_test.rb b/test/models/key_test.rb index b16e52c0..65deaf01 100644 --- a/test/models/key_test.rb +++ b/test/models/key_test.rb @@ -48,44 +48,4 @@ class KeyTest < ActiveSupport::TestCase key = Key.new(name: "hdm::integer", environment: @environment) assert_equal "hdm::integer", key.to_s end - - test "#lookup_options favors literal match over regexp" do - key = Key.new(name: "hdm::integer", environment: @environment) - hiera_data = Minitest::Mock.new - lookup_options = { - "hdm.*" => {"merge" => "unique"}, - "hdm::integer" => {"merge" => "deep"}, - ".*integer" => {"merge" => "hash"} - } - hiera_data.expect(:lookup_options, lookup_options, [Hash]) - key.stub(:hiera_data, hiera_data) do - assert_equal "deep", key.lookup_options(@node) - end - hiera_data.verify - end - - test "#lookup_options uses first regexp match if no literal match possible" do - key = Key.new(name: "hdm::integer", environment: @environment) - hiera_data = Minitest::Mock.new - lookup_options = { - "hdm.*" => {"merge" => "unique"}, - "otherkey" => {"merge" => "deep"}, - ".*integer" => {"merge" => "hash"} - } - hiera_data.expect(:lookup_options, lookup_options, [Hash]) - key.stub(:hiera_data, hiera_data) do - assert_equal "unique", key.lookup_options(@node) - end - hiera_data.verify - end - - test "#lookup_options defaults to `first`" do - key = Key.new(name: "hdm::integer", environment: @environment) - hiera_data = Minitest::Mock.new - hiera_data.expect(:lookup_options, {}, [Hash]) - key.stub(:hiera_data, hiera_data) do - assert_equal "first", key.lookup_options(@node) - end - hiera_data.verify - end end