hiera 2.0.0-x86-mingw32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +202 -0
  3. data/LICENSE +18 -0
  4. data/README.md +276 -0
  5. data/bin/hiera +248 -0
  6. data/lib/hiera.rb +115 -0
  7. data/lib/hiera/backend.rb +325 -0
  8. data/lib/hiera/backend/json_backend.rb +58 -0
  9. data/lib/hiera/backend/yaml_backend.rb +63 -0
  10. data/lib/hiera/config.rb +90 -0
  11. data/lib/hiera/console_logger.rb +13 -0
  12. data/lib/hiera/error.rb +4 -0
  13. data/lib/hiera/fallback_logger.rb +41 -0
  14. data/lib/hiera/filecache.rb +86 -0
  15. data/lib/hiera/interpolate.rb +98 -0
  16. data/lib/hiera/noop_logger.rb +8 -0
  17. data/lib/hiera/puppet_logger.rb +17 -0
  18. data/lib/hiera/recursive_guard.rb +20 -0
  19. data/lib/hiera/util.rb +47 -0
  20. data/lib/hiera/version.rb +89 -0
  21. data/spec/spec_helper.rb +78 -0
  22. data/spec/unit/backend/json_backend_spec.rb +85 -0
  23. data/spec/unit/backend/yaml_backend_spec.rb +138 -0
  24. data/spec/unit/backend_spec.rb +743 -0
  25. data/spec/unit/config_spec.rb +118 -0
  26. data/spec/unit/console_logger_spec.rb +19 -0
  27. data/spec/unit/fallback_logger_spec.rb +80 -0
  28. data/spec/unit/filecache_spec.rb +142 -0
  29. data/spec/unit/fixtures/interpolate/config/hiera.yaml +6 -0
  30. data/spec/unit/fixtures/interpolate/data/niltest.yaml +2 -0
  31. data/spec/unit/fixtures/interpolate/data/recursive.yaml +3 -0
  32. data/spec/unit/fixtures/override/config/hiera.yaml +5 -0
  33. data/spec/unit/fixtures/override/data/alternate.yaml +1 -0
  34. data/spec/unit/fixtures/override/data/common.yaml +2 -0
  35. data/spec/unit/hiera_spec.rb +81 -0
  36. data/spec/unit/interpolate_spec.rb +36 -0
  37. data/spec/unit/puppet_logger_spec.rb +31 -0
  38. data/spec/unit/util_spec.rb +49 -0
  39. data/spec/unit/version_spec.rb +44 -0
  40. metadata +142 -0
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # CLI client for Hiera.
4
+ #
5
+ # To lookup the 'release' key for a node given Puppet YAML facts:
6
+ #
7
+ # $ hiera release 'rel/%{location}' --yaml some.node.yaml
8
+ #
9
+ # If the node yaml had a location fact the default would match that
10
+ # else you can supply scope values on the command line
11
+ #
12
+ # $ hiera release 'rel/%{location}' location=dc2 --yaml some.node.yaml
13
+
14
+ # Bundler and rubygems maintain a set of directories from which to
15
+ # load gems. If Bundler is loaded, let it determine what can be
16
+ # loaded. If it's not loaded, then use rubygems. But do this before
17
+ # loading any hiera code, so that our gem loading system is sane.
18
+ if not defined? ::Bundler
19
+ begin
20
+ require 'rubygems'
21
+ rescue LoadError
22
+ end
23
+ end
24
+ require 'hiera'
25
+ require 'hiera/util'
26
+ require 'optparse'
27
+ require 'pp'
28
+
29
+ options = {
30
+ :default => nil,
31
+ :config => File.join(Hiera::Util.config_dir, 'hiera.yaml'),
32
+ :scope => {},
33
+ :key => nil,
34
+ :verbose => false,
35
+ :resolution_type => :priority,
36
+ :format => :ruby
37
+ }
38
+
39
+ initial_scopes = Array.new
40
+
41
+ # Loads the scope from YAML or JSON files
42
+ def load_scope(source, type=:yaml)
43
+ case type
44
+ when :mcollective
45
+ begin
46
+ require 'mcollective'
47
+
48
+ include MCollective::RPC
49
+
50
+ util = rpcclient("rpcutil")
51
+ util.progress = false
52
+ nodestats = util.custom_request("inventory", {}, source, {"identity" => source}).first
53
+
54
+ raise "Failed to retrieve facts for node #{source}: #{nodestats[:statusmsg]}" unless nodestats[:statuscode] == 0
55
+
56
+ scope = nodestats[:data][:facts]
57
+ rescue Exception => e
58
+ STDERR.puts "MCollective lookup failed: #{e.class}: #{e}"
59
+ exit 1
60
+ end
61
+
62
+ when :yaml
63
+ raise "Cannot find scope #{type} file #{source}" unless File.exist?(source)
64
+
65
+ require 'yaml'
66
+
67
+ # Attempt to load puppet in case we're going to be fed
68
+ # Puppet yaml files
69
+ begin
70
+ require 'puppet'
71
+ rescue
72
+ end
73
+
74
+ scope = YAML.load_file(source)
75
+
76
+ # Puppet makes dumb yaml files that do not promote data reuse.
77
+ scope = scope.values if scope.is_a?(Puppet::Node::Facts)
78
+
79
+ when :json
80
+ raise "Cannot find scope #{type} file #{source}" unless File.exist?(source)
81
+
82
+ require 'json'
83
+
84
+ scope = JSON.load(File.read(source))
85
+
86
+ when :inventory_service
87
+ # For this to work the machine running the hiera command needs access to
88
+ # /facts REST endpoint on your inventory server. This access is
89
+ # controlled in auth.conf and identification is by the certname of the
90
+ # machine running hiera commands.
91
+ #
92
+ # Another caveat is that if your inventory server isn't at the short dns
93
+ # name of 'puppet' you will need to set the inventory_sever option in
94
+ # your puppet.conf. Set it in either the master or main sections. It
95
+ # is fine to have the inventory_server option set even if the config
96
+ # doesn't have the fact_terminus set to rest.
97
+ begin
98
+ require 'puppet/util/run_mode'
99
+ $puppet_application_mode = Puppet::Util::RunMode[:master]
100
+ require 'puppet'
101
+ Puppet.settings.parse
102
+ Puppet::Node::Facts.indirection.terminus_class = :rest
103
+ scope = YAML.load(Puppet::Node::Facts.indirection.find(source).to_yaml)
104
+ # Puppet makes dumb yaml files that do not promote data reuse.
105
+ scope = scope.values if scope.is_a?(Puppet::Node::Facts)
106
+ rescue Exception => e
107
+ STDERR.puts "Puppet inventory service lookup failed: #{e.class}: #{e}"
108
+ exit 1
109
+ end
110
+ else
111
+ raise "Don't know how to load data type #{type}"
112
+ end
113
+
114
+ raise "Scope from #{type} file #{source} should be a Hash" unless scope.is_a?(Hash)
115
+
116
+ scope
117
+ end
118
+
119
+ def output_answer(ans, format)
120
+ case format
121
+ when :json
122
+ require 'json'
123
+ puts JSON.dump(ans)
124
+ when :ruby
125
+ if ans.is_a?(String)
126
+ puts ans
127
+ else
128
+ pp ans
129
+ end
130
+ when :yaml
131
+ require 'yaml'
132
+ puts ans.to_yaml
133
+ else
134
+ STDERR.puts "Unknown output format: #{v}"
135
+ exit 1
136
+ end
137
+ end
138
+
139
+ OptionParser.new do |opts|
140
+ opts.banner = "Usage: hiera [options] key [default value] [variable='text'...]\n\nThe default value will be used if no value is found for the key. Scope variables\nwill be interpolated into %{variable} placeholders in the hierarchy and in\nreturned values.\n\n"
141
+
142
+ opts.on("--version", "-V", "Version information") do
143
+ puts Hiera.version
144
+ exit
145
+ end
146
+
147
+ opts.on("--debug", "-d", "Show debugging information") do
148
+ options[:verbose] = true
149
+ end
150
+
151
+ opts.on("--array", "-a", "Return all values as an array") do
152
+ options[:resolution_type] = :array
153
+ end
154
+
155
+ opts.on("--hash", "-h", "Return all values as a hash") do
156
+ options[:resolution_type] = :hash
157
+ end
158
+
159
+ opts.on("--config CONFIG", "-c", "Configuration file") do |v|
160
+ if File.exist?(v)
161
+ options[:config] = v
162
+ else
163
+ STDERR.puts "Cannot find config file: #{v}"
164
+ exit 1
165
+ end
166
+ end
167
+
168
+ opts.on("--json SCOPE", "-j", "JSON format file to load scope from") do |v|
169
+ initial_scopes << { :type => :json, :value => v, :name => "JSON" }
170
+ end
171
+
172
+ opts.on("--yaml SCOPE", "-y", "YAML format file to load scope from") do |v|
173
+ initial_scopes << { :type => :yaml, :value => v, :name => "YAML" }
174
+ end
175
+
176
+ opts.on("--mcollective IDENTITY", "-m", "Use facts from a node (via mcollective) as scope") do |v|
177
+ initial_scopes << { :type => :mcollective, :value => v, :name => "Mcollective" }
178
+ end
179
+
180
+ opts.on("--inventory_service IDENTITY", "-i", "Use facts from a node (via Puppet's inventory service) as scope") do |v|
181
+ initial_scopes << { :type => :inventory_service, :value => v, :name => "Puppet inventory service" }
182
+ end
183
+
184
+ opts.on("--format TYPE", "-f", "Output the result in a specific format (ruby, yaml or json); default is 'ruby'") do |v|
185
+ options[:format] = case v
186
+ when 'json', 'ruby', 'yaml'
187
+ v.to_sym
188
+ else
189
+ STDERR.puts "Unknown output format: #{v}"
190
+ exit 1
191
+ end
192
+ end
193
+ end.parse!
194
+
195
+ unless initial_scopes.empty?
196
+ initial_scopes.each { |this_scope|
197
+ # Load initial scope
198
+ begin
199
+ options[:scope] = load_scope(this_scope[:value], this_scope[:type])
200
+ rescue Exception => e
201
+ STDERR.puts "Could not load #{this_scope[:name]} scope: #{e.class}: #{e}"
202
+ exit 1
203
+ end
204
+ }
205
+ end
206
+
207
+ # arguments can be:
208
+ #
209
+ # key default var=val another=val
210
+ #
211
+ # The var=val's assign scope
212
+ unless ARGV.empty?
213
+ options[:key] = ARGV.delete_at(0)
214
+
215
+ ARGV.each do |arg|
216
+ if arg =~ /^(.+?)=(.+?)$/
217
+ options[:scope][$1] = $2
218
+ else
219
+ unless options[:default]
220
+ options[:default] = arg.dup
221
+ else
222
+ STDERR.puts "Don't know how to parse scope argument: #{arg}"
223
+ end
224
+ end
225
+ end
226
+ else
227
+ STDERR.puts "Please supply a data item to look up"
228
+ exit 1
229
+ end
230
+
231
+ begin
232
+ hiera = Hiera.new(:config => options[:config])
233
+ rescue Exception => e
234
+ if options[:verbose]
235
+ raise
236
+ else
237
+ STDERR.puts "Failed to start Hiera: #{e.class}: #{e}"
238
+ exit 1
239
+ end
240
+ end
241
+
242
+ unless options[:verbose]
243
+ Hiera.logger = "noop"
244
+ end
245
+
246
+ ans = hiera.lookup(options[:key], options[:default], options[:scope], nil, options[:resolution_type])
247
+
248
+ output_answer(ans, options[:format])
@@ -0,0 +1,115 @@
1
+ require 'yaml'
2
+
3
+ class Hiera
4
+ require "hiera/error"
5
+ require "hiera/version"
6
+ require "hiera/config"
7
+ require "hiera/util"
8
+ require "hiera/backend"
9
+ require "hiera/console_logger"
10
+ require "hiera/puppet_logger"
11
+ require "hiera/noop_logger"
12
+ require "hiera/fallback_logger"
13
+ require "hiera/filecache"
14
+
15
+ class << self
16
+ attr_reader :logger
17
+
18
+ # Loggers are pluggable, just provide a class called
19
+ # Hiera::Foo_logger and respond to :warn and :debug
20
+ #
21
+ # See hiera-puppet for an example that uses the Puppet
22
+ # loging system instead of our own
23
+ def logger=(logger)
24
+ require "hiera/#{logger}_logger"
25
+
26
+ @logger = Hiera::FallbackLogger.new(
27
+ Hiera.const_get("#{logger.capitalize}_logger"),
28
+ Hiera::Console_logger)
29
+ rescue Exception => e
30
+ @logger = Hiera::Console_logger
31
+ warn("Failed to load #{logger} logger: #{e.class}: #{e}")
32
+ end
33
+
34
+ def warn(msg); @logger.warn(msg); end
35
+ def debug(msg); @logger.debug(msg); end
36
+ end
37
+
38
+ attr_reader :options, :config
39
+
40
+ # If the config option is a string its assumed to be a filename,
41
+ # else a hash of what would have been in the YAML config file
42
+ def initialize(options={})
43
+ options[:config] ||= File.join(Util.config_dir, 'hiera.yaml')
44
+
45
+ @config = Config.load(options[:config])
46
+
47
+ Config.load_backends
48
+ end
49
+
50
+ # Calls the backends to do the actual lookup.
51
+ #
52
+ # The _scope_ can be anything that responds to `[]`, if you have input
53
+ # data like a Puppet Scope that does not you can wrap that data in a
54
+ # class that has a `[]` method that fetches the data from your source.
55
+ # See hiera-puppet for an example of this.
56
+ #
57
+ # The order-override will insert as first in the hierarchy a data source
58
+ # of your choice.
59
+ #
60
+ # Possible values for the _resolution_type_ parameter:
61
+ #
62
+ # - _:priority_ - This is the default. First found value is returned and no merge is performed
63
+ # - _:array_ - An array merge lookup assembles a value from every matching level of the hierarchy. It retrieves all
64
+ # of the (string or array) values for a given key, then flattens them into a single array of unique values.
65
+ # If _priority_ lookup can be thought of as a “default with overrides” pattern, _array_ merge lookup can be though
66
+ # of as “default with additions.”
67
+ # - _:hash_ - A hash merge lookup assembles a value from every matching level of the hierarchy. It retrieves all of
68
+ # the (hash) values for a given key, then merges the hashes into a single hash. Hash merge lookups will fail with
69
+ # an error if any of the values found in the data sources are strings or arrays. It only works when every value
70
+ # found is a hash. The actual merge behavior is determined by looking up the keys `:merge_behavior` and
71
+ # `:deep_merge_options` in the Hiera config. `:merge_behavior` can be set to `:deep`, :deeper` or `:native`
72
+ # (explained in detail below).
73
+ # - _{ deep merge options }_ - Configured values for `:merge_behavior` and `:deep_merge_options`will be completely
74
+ # ignored. Instead the _resolution_type_ will be a `:hash` merge where the `:merge_behavior` will be the value
75
+ # keyed by `:behavior` in the given hash and the `:deep_merge_options` will be the remaining top level entries of
76
+ # that same hash.
77
+ #
78
+ # Valid behaviors for the _:hash_ resolution type:
79
+ #
80
+ # - _native_ - Performs a simple hash-merge by overwriting keys of lower lookup priority.
81
+ # - _deeper_ - In a deeper hash merge, Hiera recursively merges keys and values in each source hash. For each key,
82
+ # if the value is:
83
+ # - only present in one source hash, it goes into the final hash.
84
+ # - a string/number/boolean and exists in two or more source hashes, the highest priority value goes into
85
+ # the final hash.
86
+ # - an array and exists in two or more source hashes, the values from each source are merged into a single
87
+ # array and de-duplicated (but not automatically flattened, as in an array merge lookup).
88
+ # - a hash and exists in two or more source hashes, the values from each source are recursively merged, as
89
+ # though they were source hashes.
90
+ # - mismatched between two or more source hashes, we haven’t validated the behavior. It should act as
91
+ # described in the deep_merge gem documentation.
92
+ # - _deep_ - In a deep hash merge, Hiera behaves the same as for _deeper_, except that when a string/number/boolean
93
+ # exists in two or more source hashes, the lowest priority value goes into the final hash. This is considered
94
+ # largely useless and should be avoided. Use _deeper_ instead.
95
+ #
96
+ # The _merge_ can be given as a hash with the mandatory key `:strategy` to denote the actual strategy. This
97
+ # is useful for the `:deeper` and `:deep` strategy since they can use additional options to control the behavior.
98
+ # The options can be passed as top level keys in the `merge` parameter when it is a given as a hash. Recognized
99
+ # options are:
100
+ #
101
+ # - 'knockout_prefix' Set to string value to signify prefix which deletes elements from existing element. Defaults is _undef_
102
+ # - 'sort_merged_arrays' Set to _true_ to sort all arrays that are merged together. Default is _false_
103
+ # - 'unpack_arrays' Set to string value used as a deliminator to join all array values and then split them again. Default is _undef_
104
+ # - 'merge_hash_arrays' Set to _true_ to merge hashes within arrays. Default is _false_
105
+ #
106
+ # @param key [String] The key to lookup
107
+ # @param default [Object,nil] The value to return when there is no match for _key_
108
+ # @param scope [#[],nil] The scope to use for the lookup
109
+ # @param order_override [#[]] An override that will considered the first source of lookup
110
+ # @param resolution_type [String,Hash<Symbol,String>] Symbolic resolution type or deep merge configuration
111
+ # @return [Object] The found value or the given _default_ value
112
+ def lookup(key, default, scope, order_override=nil, resolution_type=:priority)
113
+ Backend.lookup(key, default, scope, order_override, resolution_type)
114
+ end
115
+ end
@@ -0,0 +1,325 @@
1
+ require 'hiera/util'
2
+ require 'hiera/interpolate'
3
+
4
+ begin
5
+ require 'deep_merge'
6
+ rescue LoadError
7
+ end
8
+
9
+ class Hiera
10
+ module Backend
11
+ class Backend1xWrapper
12
+ def initialize(wrapped)
13
+ @wrapped = wrapped
14
+ end
15
+
16
+ def lookup(key, scope, order_override, resolution_type, context)
17
+ Hiera.debug("Using Hiera 1.x backend API to access instance of class #{@wrapped.class.name}. Lookup recursion will not be detected")
18
+ value = @wrapped.lookup(key, scope, order_override, resolution_type.is_a?(Hash) ? :hash : resolution_type)
19
+
20
+ # The most likely cause when an old backend returns nil is that the key was not found. In any case, it is
21
+ # impossible to know the difference between that and a found nil. The throw here preserves the old behavior.
22
+ throw (:no_such_key) if value.nil?
23
+ value
24
+ end
25
+ end
26
+
27
+ class << self
28
+ # Data lives in /var/lib/hiera by default. If a backend
29
+ # supplies a datadir in the config it will be used and
30
+ # subject to variable expansion based on scope
31
+ def datadir(backend, scope)
32
+ backend = backend.to_sym
33
+
34
+ if Config[backend] && Config[backend][:datadir]
35
+ dir = Config[backend][:datadir]
36
+ else
37
+ dir = Hiera::Util.var_dir
38
+ end
39
+
40
+ if !dir.is_a?(String)
41
+ raise(Hiera::InvalidConfigurationError,
42
+ "datadir for #{backend} cannot be an array")
43
+ end
44
+
45
+ parse_string(dir, scope)
46
+ end
47
+
48
+ # Finds the path to a datafile based on the Backend#datadir
49
+ # and extension
50
+ #
51
+ # If the file is not found nil is returned
52
+ def datafile(backend, scope, source, extension)
53
+ datafile_in(datadir(backend, scope), source, extension)
54
+ end
55
+
56
+ # @api private
57
+ def datafile_in(datadir, source, extension)
58
+ file = File.join(datadir, "#{source}.#{extension}")
59
+
60
+ if File.exist?(file)
61
+ file
62
+ else
63
+ Hiera.debug("Cannot find datafile #{file}, skipping")
64
+ nil
65
+ end
66
+ end
67
+
68
+ # Constructs a list of data sources to search
69
+ #
70
+ # If you give it a specific hierarchy it will just use that
71
+ # else it will use the global configured one, failing that
72
+ # it will just look in the 'common' data source.
73
+ #
74
+ # An override can be supplied that will be pre-pended to the
75
+ # hierarchy.
76
+ #
77
+ # The source names will be subject to variable expansion based
78
+ # on scope
79
+ def datasources(scope, override=nil, hierarchy=nil)
80
+ if hierarchy
81
+ hierarchy = [hierarchy]
82
+ elsif Config.include?(:hierarchy)
83
+ hierarchy = [Config[:hierarchy]].flatten
84
+ else
85
+ hierarchy = ["common"]
86
+ end
87
+
88
+ hierarchy.insert(0, override) if override
89
+
90
+ hierarchy.flatten.map do |source|
91
+ source = parse_string(source, scope, {}, :order_override => override)
92
+ yield(source) unless source == "" or source =~ /(^\/|\/\/|\/$)/
93
+ end
94
+ end
95
+
96
+ # Constructs a list of data files to search
97
+ #
98
+ # If you give it a specific hierarchy it will just use that
99
+ # else it will use the global configured one, failing that
100
+ # it will just look in the 'common' data source.
101
+ #
102
+ # An override can be supplied that will be pre-pended to the
103
+ # hierarchy.
104
+ #
105
+ # The source names will be subject to variable expansion based
106
+ # on scope
107
+ #
108
+ # Only files that exist will be returned. If the file is missing, then
109
+ # the block will not receive the file.
110
+ #
111
+ # @yield [String, String] the source string and the name of the resulting file
112
+ # @api public
113
+ def datasourcefiles(backend, scope, extension, override=nil, hierarchy=nil)
114
+ datadir = Backend.datadir(backend, scope)
115
+ Backend.datasources(scope, override, hierarchy) do |source|
116
+ Hiera.debug("Looking for data source #{source}")
117
+ file = datafile_in(datadir, source, extension)
118
+
119
+ if file
120
+ yield source, file
121
+ end
122
+ end
123
+ end
124
+
125
+ # Parse a string like <code>'%{foo}'</code> against a supplied
126
+ # scope and additional scope. If either scope or
127
+ # extra_scope includes the variable 'foo', then it will
128
+ # be replaced else an empty string will be placed.
129
+ #
130
+ # If both scope and extra_data has "foo", then the value in scope
131
+ # will be used.
132
+ #
133
+ # @param data [String] The string to perform substitutions on.
134
+ # This will not be modified, instead a new string will be returned.
135
+ # @param scope [#[]] The primary source of data for substitutions.
136
+ # @param extra_data [#[]] The secondary source of data for substitutions.
137
+ # @param context [#[]] Context can include :recurse_guard and :order_override.
138
+ # @return [String] A copy of the data with all instances of <code>%{...}</code> replaced.
139
+ #
140
+ # @api public
141
+ def parse_string(data, scope, extra_data={}, context={:recurse_guard => nil, :order_override => nil})
142
+ Hiera::Interpolate.interpolate(data, scope, extra_data, context)
143
+ end
144
+
145
+ # Parses a answer received from data files
146
+ #
147
+ # Ultimately it just pass the data through parse_string but
148
+ # it makes some effort to handle arrays of strings as well
149
+ def parse_answer(data, scope, extra_data={}, context={:recurse_guard => nil, :order_override => nil})
150
+ if data.is_a?(Numeric) or data.is_a?(TrueClass) or data.is_a?(FalseClass)
151
+ return data
152
+ elsif data.is_a?(String)
153
+ return parse_string(data, scope, extra_data, context)
154
+ elsif data.is_a?(Hash)
155
+ answer = {}
156
+ data.each_pair do |key, val|
157
+ interpolated_key = parse_string(key, scope, extra_data, context)
158
+ answer[interpolated_key] = parse_answer(val, scope, extra_data, context)
159
+ end
160
+
161
+ return answer
162
+ elsif data.is_a?(Array)
163
+ answer = []
164
+ data.each do |item|
165
+ answer << parse_answer(item, scope, extra_data, context)
166
+ end
167
+
168
+ return answer
169
+ end
170
+ end
171
+
172
+ def resolve_answer(answer, resolution_type)
173
+ case resolution_type
174
+ when :array
175
+ [answer].flatten.uniq.compact
176
+ when :hash
177
+ answer # Hash structure should be preserved
178
+ else
179
+ answer
180
+ end
181
+ end
182
+
183
+ # Merges two hashes answers with the given or configured merge behavior. Behavior can be given
184
+ # by passing _resolution_type_ as a Hash
185
+ #
186
+ # :merge_behavior: {:native|:deep|:deeper}
187
+ #
188
+ # Deep merge options use the Hash utility function provided by [deep_merge](https://github.com/danielsdeleo/deep_merge)
189
+ #
190
+ # :native => Native Hash.merge
191
+ # :deep => Use Hash.deep_merge
192
+ # :deeper => Use Hash.deep_merge!
193
+ #
194
+ # @param left [Hash] left side of the merge
195
+ # @param right [Hash] right side of the merge
196
+ # @param resolution_type [String,Hash] The merge type, or if hash, the merge behavior and options
197
+ # @return [Hash] The merged result
198
+ # @see Hiera#lookup
199
+ #
200
+ def merge_answer(left,right,resolution_type=nil)
201
+ behavior, options =
202
+ if resolution_type.is_a?(Hash)
203
+ merge = resolution_type.clone
204
+ [merge.delete(:behavior), merge]
205
+ else
206
+ [Config[:merge_behavior], Config[:deep_merge_options] || {}]
207
+ end
208
+
209
+ case behavior
210
+ when :deeper,'deeper'
211
+ left.deep_merge!(right, options)
212
+ when :deep,'deep'
213
+ left.deep_merge(right, options)
214
+ else # Native and undefined
215
+ left.merge(right)
216
+ end
217
+ end
218
+
219
+ # Calls out to all configured backends in the order they
220
+ # were specified. The first one to answer will win.
221
+ #
222
+ # This lets you declare multiple backends, a possible
223
+ # use case might be in Puppet where a Puppet module declares
224
+ # default data using in-module data while users can override
225
+ # using JSON/YAML etc. By layering the backends and putting
226
+ # the Puppet one last you can override module author data
227
+ # easily.
228
+ #
229
+ # Backend instances are cached so if you need to connect to any
230
+ # databases then do so in your constructor, future calls to your
231
+ # backend will not create new instances
232
+
233
+ # @param key [String] The key to lookup
234
+ # @param scope [#[]] The primary source of data for substitutions.
235
+ # @param order_override [#[],nil] An override that will be pre-pended to the hierarchy definition.
236
+ # @param resolution_type [Symbol,Hash,nil] One of :hash, :array,:priority or a Hash with deep merge behavior and options
237
+ # @param context [#[]] Context used for internal processing
238
+ # @return [Object] The value that corresponds to the given key or nil if no such value cannot be found
239
+ #
240
+ def lookup(key, default, scope, order_override, resolution_type, context = {:recurse_guard => nil})
241
+ @backends ||= {}
242
+ answer = nil
243
+
244
+ # order_override is kept as an explicit argument for backwards compatibility, but should be specified
245
+ # in the context for internal handling.
246
+ context ||= {}
247
+ order_override ||= context[:order_override]
248
+ context[:order_override] ||= order_override
249
+
250
+ strategy = resolution_type.is_a?(Hash) ? :hash : resolution_type
251
+
252
+ segments = key.split('.')
253
+ subsegments = nil
254
+ if segments.size > 1
255
+ raise ArgumentError, "Resolution type :#{strategy} is illegal when doing segmented key lookups" unless strategy.nil? || strategy == :priority
256
+ subsegments = segments.drop(1)
257
+ end
258
+
259
+ found = false
260
+ Config[:backends].each do |backend|
261
+ backend_constant = "#{backend.capitalize}_backend"
262
+ if constants.include?(backend_constant) || constants.include?(backend_constant.to_sym)
263
+ backend = (@backends[backend] ||= find_backend(backend_constant))
264
+ found_in_backend = false
265
+ new_answer = catch(:no_such_key) do
266
+ value = backend.lookup(segments[0], scope, order_override, resolution_type, context)
267
+ value = qualified_lookup(subsegments, value) unless subsegments.nil?
268
+ found_in_backend = true
269
+ value
270
+ end
271
+ next unless found_in_backend
272
+ found = true
273
+
274
+ case strategy
275
+ when :array
276
+ raise Exception, "Hiera type mismatch for key '#{key}': expected Array and got #{new_answer.class}" unless new_answer.kind_of? Array or new_answer.kind_of? String
277
+ answer ||= []
278
+ answer << new_answer
279
+ when :hash
280
+ raise Exception, "Hiera type mismatch for key '#{key}': expected Hash and got #{new_answer.class}" unless new_answer.kind_of? Hash
281
+ answer ||= {}
282
+ answer = merge_answer(new_answer, answer, resolution_type)
283
+ else
284
+ answer = new_answer
285
+ break
286
+ end
287
+ end
288
+ end
289
+
290
+ answer = resolve_answer(answer, strategy) unless answer.nil?
291
+ answer = parse_string(default, scope, {}, context) if !found && default.is_a?(String)
292
+
293
+ return default if !found && answer.nil?
294
+ return answer
295
+ end
296
+
297
+ def clear!
298
+ @backends = {}
299
+ end
300
+
301
+ def qualified_lookup(segments, hash)
302
+ value = hash
303
+ segments.each do |segment|
304
+ throw :no_such_key if value.nil?
305
+ if segment =~ /^[0-9]+$/
306
+ segment = segment.to_i
307
+ raise Exception, "Hiera type mismatch: Got #{value.class.name} when Array was expected enable lookup using key '#{segment}'" unless value.instance_of?(Array)
308
+ throw :no_such_key unless segment < value.size
309
+ else
310
+ raise Exception, "Hiera type mismatch: Got #{value.class.name} when a non Array object that responds to '[]' was expected to enable lookup using key '#{segment}'" unless value.respond_to?(:'[]') && !value.instance_of?(Array);
311
+ throw :no_such_key unless value.include?(segment)
312
+ end
313
+ value = value[segment]
314
+ end
315
+ value
316
+ end
317
+
318
+ def find_backend(backend_constant)
319
+ backend = Backend.const_get(backend_constant).new
320
+ return backend.method(:lookup).arity == 4 ? Backend1xWrapper.new(backend) : backend
321
+ end
322
+ private :find_backend
323
+ end
324
+ end
325
+ end