hiera 2.0.0-x86-mingw32

Sign up to get free protection for your applications and to get access to all the features.
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