File: backend.rb

package info (click to toggle)
hiera 3.2.0-2
  • links: PTS, VCS
  • area: main
  • in suites: buster, stretch
  • size: 628 kB
  • ctags: 162
  • sloc: ruby: 3,056; makefile: 31; sh: 17
file content (356 lines) | stat: -rw-r--r-- 14,128 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
require 'hiera/util'
require 'hiera/interpolate'

begin
  require 'deep_merge/rails_compat'
rescue LoadError
end

class Hiera
  module Backend
    class Backend1xWrapper
      def initialize(wrapped)
        @wrapped = wrapped
      end

      def lookup(key, scope, order_override, resolution_type, context)
        Hiera.debug("Using Hiera 1.x backend API to access instance of class #{@wrapped.class.name}. Lookup recursion will not be detected")
        value = @wrapped.lookup(key, scope, order_override, resolution_type.is_a?(Hash) ? :hash : resolution_type)

        # The most likely cause when an old backend returns nil is that the key was not found. In any case, it is
        # impossible to know the difference between that and a found nil. The throw here preserves the old behavior.
        throw (:no_such_key) if value.nil?
        value
      end
    end

    class << self
      # Data lives in /var/lib/hiera by default.  If a backend
      # supplies a datadir in the config it will be used and
      # subject to variable expansion based on scope
      def datadir(backend, scope)
        backend = backend.to_sym

        if Config[backend] && Config[backend][:datadir]
          dir = Config[backend][:datadir]
        else
          dir = Hiera::Util.var_dir
        end

        if !dir.is_a?(String)
          raise(Hiera::InvalidConfigurationError,
                "datadir for #{backend} cannot be an array")
        end

        interpolate_config(dir, scope, nil)
      end

      # Finds the path to a datafile based on the Backend#datadir
      # and extension
      #
      # If the file is not found nil is returned
      def datafile(backend, scope, source, extension)
        datafile_in(datadir(backend, scope), source, extension)
      end

      # @api private
      def datafile_in(datadir, source, extension)
        file = File.join(datadir, "#{source}.#{extension}")

        if File.exist?(file)
          file
        else
          Hiera.debug("Cannot find datafile #{file}, skipping")
          nil
        end
      end

      # Constructs a list of data sources to search
      #
      # If you give it a specific hierarchy it will just use that
      # else it will use the global configured one, failing that
      # it will just look in the 'common' data source.
      #
      # An override can be supplied that will be pre-pended to the
      # hierarchy.
      #
      # The source names will be subject to variable expansion based
      # on scope
      def datasources(scope, override=nil, hierarchy=nil)
        if hierarchy
          hierarchy = [hierarchy]
        elsif Config.include?(:hierarchy)
          hierarchy = [Config[:hierarchy]].flatten
        else
          hierarchy = ["common"]
        end

        hierarchy.insert(0, override) if override

        hierarchy.flatten.map do |source|
          source = interpolate_config(source, scope, override)
          yield(source) unless source == "" or source =~ /(^\/|\/\/|\/$)/
        end
      end

      # Constructs a list of data files to search
      #
      # If you give it a specific hierarchy it will just use that
      # else it will use the global configured one, failing that
      # it will just look in the 'common' data source.
      #
      # An override can be supplied that will be pre-pended to the
      # hierarchy.
      #
      # The source names will be subject to variable expansion based
      # on scope
      #
      # Only files that exist will be returned. If the file is missing, then
      # the block will not receive the file.
      #
      # @yield [String, String] the source string and the name of the resulting file
      # @api public
      def datasourcefiles(backend, scope, extension, override=nil, hierarchy=nil)
        datadir = Backend.datadir(backend, scope)
        Backend.datasources(scope, override, hierarchy) do |source|
          Hiera.debug("Looking for data source #{source}")
          file = datafile_in(datadir, source, extension)

          if file
            yield source, file
          end
        end
      end

      # Parse a string like <code>'%{foo}'</code> against a supplied
      # scope and additional scope.  If either scope or
      # extra_scope includes the variable 'foo', then it will
      # be replaced else an empty string will be placed.
      #
      # If both scope and extra_data has "foo", then the value in scope
      # will be used.
      #
      # @param data [String] The string to perform substitutions on.
      #   This will not be modified, instead a new string will be returned.
      # @param scope [#[]] The primary source of data for substitutions.
      # @param extra_data [#[]] The secondary source of data for substitutions.
      # @param context [#[]] Context can include :recurse_guard and :order_override.
      # @return [String] A copy of the data with all instances of <code>%{...}</code> replaced.
      #
      # @api public
      def parse_string(data, scope, extra_data={}, context={:recurse_guard => nil, :order_override => nil})
        Hiera::Interpolate.interpolate(data, scope, extra_data, context)
      end

      # Parses a answer received from data files
      #
      # Ultimately it just pass the data through parse_string but
      # it makes some effort to handle arrays of strings as well
      def parse_answer(data, scope, extra_data={}, context={:recurse_guard => nil, :order_override => nil})
        if data.is_a?(Numeric) or data.is_a?(TrueClass) or data.is_a?(FalseClass)
          return data
        elsif data.is_a?(String)
          return parse_string(data, scope, extra_data, context)
        elsif data.is_a?(Hash)
          answer = {}
          data.each_pair do |key, val|
            interpolated_key = parse_string(key, scope, extra_data, context)
            answer[interpolated_key] = parse_answer(val, scope, extra_data, context)
          end

          return answer
        elsif data.is_a?(Array)
          answer = []
          data.each do |item|
            answer << parse_answer(item, scope, extra_data, context)
          end

          return answer
        end
      end

      def resolve_answer(answer, resolution_type)
        case resolution_type
        when :array
          [answer].flatten.uniq.compact
        when :hash
          answer # Hash structure should be preserved
        else
          answer
        end
      end

      # Merges two hashes answers with the given or configured merge behavior. Behavior can be given
      # by passing _resolution_type_ as a Hash
      #
      #  :merge_behavior: {:native|:deep|:deeper}
      #
      # Deep merge options use the Hash utility function provided by [deep_merge](https://github.com/danielsdeleo/deep_merge)
      # It uses the compatibility mode [deep_merge](https://github.com/danielsdeleo/deep_merge#using-deep_merge-in-rails)
      #
      #  :native => Native Hash.merge
      #  :deep   => Use Hash.deeper_merge
      #  :deeper => Use Hash.deeper_merge!
      #
      # @param left [Hash] left side of the merge
      # @param right [Hash] right side of the merge
      # @param resolution_type [String,Hash] The merge type, or if hash, the merge behavior and options
      # @return [Hash] The merged result
      # @see Hiera#lookup
      #
      def merge_answer(left,right,resolution_type=nil)
        behavior, options =
          if resolution_type.is_a?(Hash)
            merge = resolution_type.clone
            [merge.delete(:behavior), merge]
          else
            [Config[:merge_behavior], Config[:deep_merge_options] || {}]
          end

        case behavior
        when :deeper,'deeper'
          left.deeper_merge!(right, options)
        when :deep,'deep'
          left.deeper_merge(right, options)
        else # Native and undefined
          left.merge(right)
        end
      end

      # Calls out to all configured backends in the order they
      # were specified.  The first one to answer will win.
      #
      # This lets you declare multiple backends, a possible
      # use case might be in Puppet where a Puppet module declares
      # default data using in-module data while users can override
      # using JSON/YAML etc.  By layering the backends and putting
      # the Puppet one last you can override module author data
      # easily.
      #
      # Backend instances are cached so if you need to connect to any
      # databases then do so in your constructor, future calls to your
      # backend will not create new instances

      # @param key [String] The key to lookup. May be quoted with single or double quotes to avoid subkey traversal on dot characters
      # @param scope [#[]] The primary source of data for substitutions.
      # @param order_override [#[],nil] An override that will be pre-pended to the hierarchy definition.
      # @param resolution_type [Symbol,Hash,nil] One of :hash, :array,:priority or a Hash with deep merge behavior and options
      # @param context [#[]] Context used for internal processing
      # @return [Object] The value that corresponds to the given key or nil if no such value cannot be found
      #
      def lookup(key, default, scope, order_override, resolution_type, context = {:recurse_guard => nil})
        @backends ||= {}
        answer = nil

        # order_override is kept as an explicit argument for backwards compatibility, but should be specified
        # in the context for internal handling.
        context ||= {}
        order_override ||= context[:order_override]
        context[:order_override] ||= order_override

        strategy = resolution_type.is_a?(Hash) ? :hash : resolution_type

        segments = Util.split_key(key) { |problem| ArgumentError.new("#{problem} in key: #{key}") }
        subsegments = nil
        if segments.size > 1
          unless strategy.nil? || strategy == :priority
            raise ArgumentError, "Resolution type :#{strategy} is illegal when accessing values using dotted keys. Offending key was '#{key}'"
          end
          subsegments = segments.drop(1)
        end

        found = false
        Config[:backends].each do |backend|
          backend_constant = "#{backend.capitalize}_backend"
          if constants.include?(backend_constant) || constants.include?(backend_constant.to_sym)
            backend = (@backends[backend] ||= find_backend(backend_constant))
            found_in_backend = false
            new_answer = catch(:no_such_key) do
              if subsegments.nil? 
                value = backend.lookup(segments[0], scope, order_override, resolution_type, context)
              elsif backend.respond_to?(:lookup_with_segments)
                value = backend.lookup_with_segments(segments, scope, order_override, resolution_type, context)
              else
                value = backend.lookup(segments[0], scope, order_override, resolution_type, context)
                value = qualified_lookup(subsegments, value, key) unless subsegments.nil?
              end
              found_in_backend = true
              value
            end
            next unless found_in_backend
            found = true

            case strategy
            when :array
              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
              answer ||= []
              answer << new_answer
            when :hash
              raise Exception, "Hiera type mismatch for key '#{key}': expected Hash and got #{new_answer.class}" unless new_answer.kind_of? Hash
              answer ||= {}
              answer = merge_answer(new_answer, answer, resolution_type)
            else
              answer = new_answer
              break
            end
          end
        end

        answer = resolve_answer(answer, strategy) unless answer.nil?
        answer = parse_string(default, scope, {}, context) if !found && default.is_a?(String)

        return default if !found && answer.nil?
        return answer
      end

      def clear!
        @backends = {}
      end

      def qualified_lookup(segments, hash, full_key = nil)
        value = hash
        segments.each do |segment|
          throw :no_such_key if value.nil?
          if segment =~ /^[0-9]+$/
            segment = segment.to_i
            unless value.instance_of?(Array)
              suffix = full_key.nil? ? '' : " from key '#{full_key}'"
              raise Exception,
                "Hiera type mismatch: Got #{value.class.name} when Array was expected to access value using '#{segment}'#{suffix}"
            end
            throw :no_such_key unless segment < value.size
          else
            unless value.respond_to?(:'[]') && !(value.instance_of?(Array) || value.instance_of?(String))
              suffix = full_key.nil? ? '' : " from key '#{full_key}'"
              raise Exception,
                "Hiera type mismatch: Got #{value.class.name} when a hash-like object was expected to access value using '#{segment}'#{suffix}"
            end
            throw :no_such_key unless value.include?(segment)
          end
          value = value[segment]
        end
        value
      end

      def find_backend(backend_constant)
        backend = Backend.const_get(backend_constant).new
        return backend.method(:lookup).arity == 4 ? Backend1xWrapper.new(backend) : backend
      end
      private :find_backend

      def interpolate_config(entry, scope, override)
        if @config_lookup_context.nil?
          @config_lookup_context = { :is_interpolate_config => true, :order_override => override, :recurse_guard => Hiera::RecursiveGuard.new }
          begin
            Hiera::Interpolate.interpolate(entry, scope, {}, @config_lookup_context)
          ensure
            @config_lookup_context = nil
          end
        else
          # Nested call (will happen when interpolate method 'hiera' is used)
          Hiera::Interpolate.interpolate(entry, scope, {}, @config_lookup_context.merge(:order_override => override))
        end
      end
    end
  end
end