diff --git a/lib/puppet/functions/mysql/normalise_and_deepmerge.rb b/lib/puppet/functions/mysql/normalise_and_deepmerge.rb new file mode 100644 index 000000000..70c03f599 --- /dev/null +++ b/lib/puppet/functions/mysql/normalise_and_deepmerge.rb @@ -0,0 +1,67 @@ +# @summary Recursively merges two or more hashes together, normalises keys with differing use of dashesh and underscores, +# then returns the resulting hash. +# +# @example +# $hash1 = {'one' => 1, 'two' => 2, 'three' => { 'four' => 4 } } +# $hash2 = {'two' => 'dos', 'three' => { 'five' => 5 } } +# $merged_hash = mysql::normalise_and_deepmerge($hash1, $hash2) +# # The resulting hash is equivalent to: +# # $merged_hash = { 'one' => 1, 'two' => 'dos', 'three' => { 'four' => 4, 'five' => 5 } } +# +# - When there is a duplicate key that is a hash, they are recursively merged. +# - When there is a duplicate key that is not a hash, the key in the rightmost hash will "win." +# - When there are conficting uses of dashes and underscores in two keys (which mysql would otherwise equate), the rightmost style will win. +# +Puppet::Functions.create_function(:'mysql::normalise_and_deepmerge') do + def normalise_and_deepmerge(*args) + if args.length < 2 + raise Puppet::ParseError, _('mysql::normalise_and_deepmerge(): wrong number of arguments (%{args_length}; must be at least 2)') % { args_length: args.length } + end + + result = {} + args.each do |arg| + next if arg.is_a?(String) && arg.empty? # empty string is synonym for puppet's undef + # If the argument was not a hash, skip it. + unless arg.is_a?(Hash) + raise Puppet::ParseError, _('mysql::normalise_and_deepmerge: unexpected argument type %{arg_class}, only expects hash arguments.') % { args_class: args.class } + end + + # We need to make a copy of the hash since it is frozen by puppet + current = deep_copy(arg) + + # Now we have to traverse our hash assigning our non-hash values + # to the matching keys in our result while following our hash values + # and repeating the process. + overlay(result, current) + end + result + end + + def normalized?(hash, key) + return true if hash.key?(key) + return false unless key =~ %r{-|_} + other_key = key.include?('-') ? key.tr('-', '_') : key.tr('_', '-') + return false unless hash.key?(other_key) + hash[key] = hash.delete(other_key) + true + end + + def overlay(hash1, hash2) + hash2.each do |key, value| + if normalized?(hash1, key) && value.is_a?(Hash) && hash1[key].is_a?(Hash) + overlay(hash1[key], value) + else + hash1[key] = value + end + end + end + + def deep_copy(inputhash) + return inputhash unless inputhash.is_a? Hash + hash = {} + inputhash.each do |k, v| + hash.store(k, deep_copy(v)) + end + hash + end +end diff --git a/manifests/backup/mysqlbackup.pp b/manifests/backup/mysqlbackup.pp index 24fc99ad7..0da68bf19 100644 --- a/manifests/backup/mysqlbackup.pp +++ b/manifests/backup/mysqlbackup.pp @@ -93,7 +93,7 @@ 'password' => $backuppassword, } } - $options = $default_options.deep_merge($mysql::server::override_options) + $options = mysql::normalise_and_deepmerge($default_options, $mysql::server::override_options) file { 'mysqlbackup-config-file': path => '/etc/mysql/conf.d/meb.cnf', diff --git a/manifests/server.pp b/manifests/server.pp index 1f3e5d3be..f70ca4146 100644 --- a/manifests/server.pp +++ b/manifests/server.pp @@ -116,7 +116,7 @@ } # Create a merged together set of options. Rightmost hashes win over left. - $options = $mysql::params::default_options.deep_merge($override_options) + $options = mysql::normalise_and_deepmerge($mysql::params::default_options, $override_options) Class['mysql::server::root_password'] -> Mysql::Db <| |> diff --git a/spec/functions/mysql_normalise_and_deepmerge_spec.rb b/spec/functions/mysql_normalise_and_deepmerge_spec.rb new file mode 100644 index 000000000..d1c94f8ff --- /dev/null +++ b/spec/functions/mysql_normalise_and_deepmerge_spec.rb @@ -0,0 +1,92 @@ +require 'spec_helper' + +describe 'mysql::normalise_and_deepmerge' do + it 'exists' do + is_expected.not_to eq(nil) + end + + it 'throws error with no arguments' do + is_expected.to run.with_params.and_raise_error(Puppet::ParseError) + end + + it 'throws error with only one argument' do + is_expected.to run.with_params('one' => 1).and_raise_error(Puppet::ParseError) + end + + it 'accepts empty strings as puppet undef' do + is_expected.to run.with_params({}, '') + end + + # rubocop:disable RSpec/NamedSubject + index_values = ['one', 'two', 'three'] + expected_values_one = ['1', '2', '2'] + it 'merge two hashes' do + new_hash = subject.execute({ 'one' => '1', 'two' => '1' }, 'two' => '2', 'three' => '2') + index_values.each_with_index do |index, expected| + expect(new_hash[index]).to eq(expected_values_one[expected]) + end + end + + it 'merges multiple hashes' do + hash = subject.execute({ 'one' => 1 }, { 'one' => '2' }, 'one' => '3') + expect(hash['one']).to eq('3') + end + + it 'accepts empty hashes' do + is_expected.to run.with_params({}, {}, {}).and_return({}) + end + + expected_values_two = [1, 2, 'four' => 4] + it 'merges subhashes' do + hash = subject.execute({ 'one' => 1 }, 'two' => 2, 'three' => { 'four' => 4 }) + index_values.each_with_index do |index, expected| + expect(hash[index]).to eq(expected_values_two[expected]) + end + end + + it 'appends to subhashes' do + hash = subject.execute({ 'one' => { 'two' => 2 } }, 'one' => { 'three' => 3 }) + expect(hash['one']).to eq('two' => 2, 'three' => 3) + end + + expected_values_three = [1, 'dos', { 'four' => 4, 'five' => 5 }] + it 'appends to subhashes 2' do + hash = subject.execute({ 'one' => 1, 'two' => 2, 'three' => { 'four' => 4 } }, 'two' => 'dos', 'three' => { 'five' => 5 }) + index_values.each_with_index do |index, expected| + expect(hash[index]).to eq(expected_values_three[expected]) + end + end + + index_values_two = ['key1', 'key2'] + expected_values_four = [{ 'a' => 1, 'b' => 99 }, 'c' => 3] + it 'appends to subhashes 3' do + hash = subject.execute({ 'key1' => { 'a' => 1, 'b' => 2 }, 'key2' => { 'c' => 3 } }, 'key1' => { 'b' => 99 }) + index_values_two.each_with_index do |index, expected| + expect(hash[index]).to eq(expected_values_four[expected]) + end + end + + it 'equates keys mod dash and underscore #value' do + hash = subject.execute({ 'a-b-c' => 1 }, 'a_b_c' => 10) + expect(hash['a_b_c']).to eq(10) + end + it 'equates keys mod dash and underscore #not' do + hash = subject.execute({ 'a-b-c' => 1 }, 'a_b_c' => 10) + expect(hash).not_to have_key('a-b-c') + end + + index_values_three = ['a_b_c', 'b-c-d'] + expected_values_five = [10, { 'e-f-g' => 3, 'c_d_e' => 12 }] + index_values_error = ['a-b-c', 'b_c_d'] + index_values_three.each_with_index do |index, expected| + it 'keeps style of the last when keys are equal mod dash and underscore #value' do + hash = subject.execute({ 'a-b-c' => 1, 'b_c_d' => { 'c-d-e' => 2, 'e-f-g' => 3 } }, 'a_b_c' => 10, 'b-c-d' => { 'c_d_e' => 12 }) + expect(hash[index]).to eq(expected_values_five[expected]) + end + it 'keeps style of the last when keys are equal mod dash and underscore #not' do + hash = subject.execute({ 'a-b-c' => 1, 'b_c_d' => { 'c-d-e' => 2, 'e-f-g' => 3 } }, 'a_b_c' => 10, 'b-c-d' => { 'c_d_e' => 12 }) + expect(hash).not_to have_key(index_values_error[expected]) + end + end + # rubocop:enable RSpec/NamedSubject +end