hiera 1.2.0.rc1 → 1.2.0.rc2
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of hiera might be problematic. Click here for more details.
- data/README.md +2 -0
- data/lib/hiera.rb +14 -12
- data/lib/hiera/backend.rb +28 -30
- data/lib/hiera/backend/json_backend.rb +8 -2
- data/lib/hiera/backend/yaml_backend.rb +10 -28
- data/lib/hiera/fallback_logger.rb +41 -0
- data/lib/hiera/filecache.rb +74 -0
- data/lib/hiera/puppet_logger.rb +4 -0
- data/lib/hiera/recursive_lookup.rb +31 -0
- data/spec/unit/backend/json_backend_spec.rb +14 -18
- data/spec/unit/backend/yaml_backend_spec.rb +35 -73
- data/spec/unit/backend_spec.rb +112 -60
- data/spec/unit/fallback_logger_spec.rb +80 -0
- data/spec/unit/filecache_spec.rb +63 -0
- data/spec/unit/hiera_spec.rb +30 -11
- data/spec/unit/puppet_logger_spec.rb +31 -0
- metadata +13 -4
data/README.md
CHANGED
data/lib/hiera.rb
CHANGED
@@ -1,14 +1,16 @@
|
|
1
1
|
require 'yaml'
|
2
2
|
|
3
3
|
class Hiera
|
4
|
-
VERSION = "1.2.0-
|
4
|
+
VERSION = "1.2.0-rc2"
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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"
|
12
14
|
|
13
15
|
class << self
|
14
16
|
attr_reader :logger
|
@@ -23,13 +25,13 @@ class Hiera
|
|
23
25
|
# See hiera-puppet for an example that uses the Puppet
|
24
26
|
# loging system instead of our own
|
25
27
|
def logger=(logger)
|
26
|
-
|
28
|
+
require "hiera/#{logger}_logger"
|
27
29
|
|
28
|
-
|
29
|
-
|
30
|
-
|
30
|
+
@logger = Hiera::FallbackLogger.new(
|
31
|
+
Hiera.const_get("#{logger.capitalize}_logger"),
|
32
|
+
Hiera::Console_logger)
|
31
33
|
rescue Exception => e
|
32
|
-
@logger = Console_logger
|
34
|
+
@logger = Hiera::Console_logger
|
33
35
|
warn("Failed to load #{logger} logger: #{e.class}: #{e}")
|
34
36
|
end
|
35
37
|
|
data/lib/hiera/backend.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
require 'hiera/util'
|
2
|
+
require 'hiera/recursive_lookup'
|
3
|
+
|
2
4
|
begin
|
3
5
|
require 'deep_merge'
|
4
6
|
rescue LoadError
|
@@ -6,6 +8,8 @@ end
|
|
6
8
|
|
7
9
|
class Hiera
|
8
10
|
module Backend
|
11
|
+
INTERPOLATION = /%\{([^\}]*)\}/
|
12
|
+
|
9
13
|
class << self
|
10
14
|
# Data lives in /var/lib/hiera by default. If a backend
|
11
15
|
# supplies a datadir in the config it will be used and
|
@@ -65,44 +69,38 @@ class Hiera
|
|
65
69
|
end
|
66
70
|
end
|
67
71
|
|
68
|
-
# Parse a string like '%{foo}' against a supplied
|
72
|
+
# Parse a string like <code>'%{foo}'</code> against a supplied
|
69
73
|
# scope and additional scope. If either scope or
|
70
|
-
# extra_scope includes the
|
74
|
+
# extra_scope includes the variable 'foo', then it will
|
71
75
|
# be replaced else an empty string will be placed.
|
72
76
|
#
|
73
|
-
# If both scope and extra_data has "foo" scope
|
74
|
-
# will
|
75
|
-
#
|
76
|
-
#
|
77
|
+
# If both scope and extra_data has "foo", then the value in scope
|
78
|
+
# will be used.
|
79
|
+
#
|
80
|
+
# @param data [String] The string to perform substitutions on.
|
81
|
+
# This will not be modified, instead a new string will be returned.
|
82
|
+
# @param scope [#[]] The primary source of data for substitutions.
|
83
|
+
# @param extra_data [#[]] The secondary source of data for substitutions.
|
84
|
+
# @return [String] A copy of the data with all instances of <code>%{...}</code> replaced.
|
85
|
+
#
|
86
|
+
# @api public
|
77
87
|
def parse_string(data, scope, extra_data={})
|
78
|
-
|
79
|
-
|
80
|
-
tdata = data.clone
|
81
|
-
|
82
|
-
if tdata.is_a?(String)
|
83
|
-
while tdata =~ /%\{(.+?)\}/
|
84
|
-
begin
|
85
|
-
var = $1
|
86
|
-
|
87
|
-
val = ""
|
88
|
-
|
89
|
-
# Puppet can return :undefined for unknown scope vars,
|
90
|
-
# If it does then we still need to evaluate extra_data
|
91
|
-
# before returning an empty string.
|
92
|
-
scope_val = scope[var]
|
93
|
-
if !scope_val.nil? && scope_val != :undefined
|
94
|
-
val = scope_val
|
95
|
-
elsif extra_data[var]
|
96
|
-
val = extra_data[var]
|
97
|
-
end
|
98
|
-
end until val != "" || var !~ /::(.+)/
|
88
|
+
interpolate(data, Hiera::RecursiveLookup.new(scope, extra_data))
|
89
|
+
end
|
99
90
|
|
100
|
-
|
91
|
+
def interpolate(data, values)
|
92
|
+
if data.is_a?(String)
|
93
|
+
data.gsub(INTERPOLATION) do
|
94
|
+
name = $1
|
95
|
+
values.lookup(name) do |value|
|
96
|
+
interpolate(value, values)
|
97
|
+
end
|
101
98
|
end
|
99
|
+
else
|
100
|
+
data
|
102
101
|
end
|
103
|
-
|
104
|
-
return tdata
|
105
102
|
end
|
103
|
+
private :interpolate
|
106
104
|
|
107
105
|
# Parses a answer received from data files
|
108
106
|
#
|
@@ -1,10 +1,12 @@
|
|
1
1
|
class Hiera
|
2
2
|
module Backend
|
3
3
|
class Json_backend
|
4
|
-
def initialize
|
4
|
+
def initialize(cache=nil)
|
5
5
|
require 'json'
|
6
6
|
|
7
7
|
Hiera.debug("Hiera JSON backend starting")
|
8
|
+
|
9
|
+
@cache = cache || Filecache.new
|
8
10
|
end
|
9
11
|
|
10
12
|
def lookup(key, scope, order_override, resolution_type)
|
@@ -17,7 +19,11 @@ class Hiera
|
|
17
19
|
|
18
20
|
jsonfile = Backend.datafile(:json, scope, source, "json") || next
|
19
21
|
|
20
|
-
|
22
|
+
next unless File.exist?(jsonfile)
|
23
|
+
|
24
|
+
data = @cache.read(jsonfile, Hash, {}) do |data|
|
25
|
+
JSON.parse(data)
|
26
|
+
end
|
21
27
|
|
22
28
|
next if data.empty?
|
23
29
|
next unless data.include?(key)
|
@@ -1,11 +1,11 @@
|
|
1
1
|
class Hiera
|
2
2
|
module Backend
|
3
3
|
class Yaml_backend
|
4
|
-
def initialize
|
4
|
+
def initialize(cache=nil)
|
5
5
|
require 'yaml'
|
6
6
|
Hiera.debug("Hiera YAML backend starting")
|
7
|
-
|
8
|
-
@cache =
|
7
|
+
|
8
|
+
@cache = cache || Filecache.new
|
9
9
|
end
|
10
10
|
|
11
11
|
def lookup(key, scope, order_override, resolution_type)
|
@@ -17,20 +17,14 @@ class Hiera
|
|
17
17
|
Hiera.debug("Looking for data source #{source}")
|
18
18
|
yamlfile = Backend.datafile(:yaml, scope, source, "yaml") || next
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
# is @data[yamlfile] populated AND is the cache stale.
|
25
|
-
if @data[yamlfile]
|
26
|
-
@data[yamlfile] = YAML.load_file(yamlfile) if stale?(yamlfile)
|
27
|
-
else
|
28
|
-
@data[yamlfile] = YAML.load_file(yamlfile)
|
20
|
+
next unless File.exist?(yamlfile)
|
21
|
+
|
22
|
+
data = @cache.read(yamlfile, Hash, {}) do |data|
|
23
|
+
YAML.load(data)
|
29
24
|
end
|
30
25
|
|
31
|
-
next if
|
32
|
-
next
|
33
|
-
next unless @data[yamlfile].include?(key)
|
26
|
+
next if data.empty?
|
27
|
+
next unless data.include?(key)
|
34
28
|
|
35
29
|
# Extra logging that we found the key. This can be outputted
|
36
30
|
# multiple times if the resolution type is array or hash but that
|
@@ -43,7 +37,7 @@ class Hiera
|
|
43
37
|
# the array
|
44
38
|
#
|
45
39
|
# for priority searches we break after the first found data item
|
46
|
-
new_answer = Backend.parse_answer(
|
40
|
+
new_answer = Backend.parse_answer(data[key], scope)
|
47
41
|
case resolution_type
|
48
42
|
when :array
|
49
43
|
raise Exception, "Hiera type mismatch: expected Array and got #{new_answer.class}" unless new_answer.kind_of? Array or new_answer.kind_of? String
|
@@ -61,18 +55,6 @@ class Hiera
|
|
61
55
|
|
62
56
|
return answer
|
63
57
|
end
|
64
|
-
|
65
|
-
def stale?(yamlfile)
|
66
|
-
# NOTE: The mtime change in a file MUST be > 1 second before being
|
67
|
-
# recognized as stale. File mtime changes within 1 second will
|
68
|
-
# not be recognized.
|
69
|
-
stat = File.stat(yamlfile)
|
70
|
-
current = { 'inode' => stat.ino, 'mtime' => stat.mtime, 'size' => stat.size }
|
71
|
-
return false if @cache[yamlfile] == current
|
72
|
-
|
73
|
-
@cache[yamlfile] = current
|
74
|
-
return true
|
75
|
-
end
|
76
58
|
end
|
77
59
|
end
|
78
60
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# Select from a given list of loggers the first one that
|
2
|
+
# it suitable and use that as the actual logger
|
3
|
+
#
|
4
|
+
# @api private
|
5
|
+
class Hiera::FallbackLogger
|
6
|
+
# Chooses the first suitable logger. For all of the loggers that are
|
7
|
+
# unsuitable it will issue a warning using the suitable logger stating that
|
8
|
+
# the unsuitable logger is not being used.
|
9
|
+
#
|
10
|
+
# @param implementations [Array<Hiera::Logger>] the implementations to choose from
|
11
|
+
# @raises when there are no suitable loggers
|
12
|
+
def initialize(*implementations)
|
13
|
+
warnings = []
|
14
|
+
@implementation = implementations.find do |impl|
|
15
|
+
if impl.respond_to?(:suitable?)
|
16
|
+
if impl.suitable?
|
17
|
+
true
|
18
|
+
else
|
19
|
+
warnings << "Not using #{impl.name}. It does not report itself to be suitable."
|
20
|
+
false
|
21
|
+
end
|
22
|
+
else
|
23
|
+
true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
if @implementation.nil?
|
28
|
+
raise "No suitable logging implementation found."
|
29
|
+
end
|
30
|
+
|
31
|
+
warnings.each { |message| warn(message) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def warn(message)
|
35
|
+
@implementation.warn(message)
|
36
|
+
end
|
37
|
+
|
38
|
+
def debug(message)
|
39
|
+
@implementation.debug(message)
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
class Hiera
|
2
|
+
class Filecache
|
3
|
+
def initialize
|
4
|
+
@cache = {}
|
5
|
+
end
|
6
|
+
|
7
|
+
# Reads a file, optionally parse it in some way check the
|
8
|
+
# output type and set a default
|
9
|
+
#
|
10
|
+
# Simply invoking it this way will return the file contents
|
11
|
+
#
|
12
|
+
# data = read("/some/file")
|
13
|
+
#
|
14
|
+
# But as most cases of file reading in hiera involves some kind
|
15
|
+
# of parsing through a serializer there's some help for those
|
16
|
+
# cases:
|
17
|
+
#
|
18
|
+
# data = read("/some/file", Hash, {}) do |data|
|
19
|
+
# JSON.parse(data)
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# In this case it will read the file, parse it using JSON then
|
23
|
+
# check that the end result is a Hash, if it's not a hash or if
|
24
|
+
# reading/parsing fails it will return {} instead
|
25
|
+
#
|
26
|
+
# Prior to calling this method you should be sure the file exist
|
27
|
+
def read(path, expected_type=nil, default=nil)
|
28
|
+
@cache[path] ||= {:data => nil, :meta => path_metadata(path)}
|
29
|
+
|
30
|
+
if File.exist?(path) && !@cache[path][:data] || stale?(path)
|
31
|
+
if block_given?
|
32
|
+
begin
|
33
|
+
@cache[path][:data] = yield(File.read(path))
|
34
|
+
rescue => e
|
35
|
+
Hiera.debug("Reading data from %s failed: %s: %S" % [path, e.class, e.to_s])
|
36
|
+
@cache[path][:data] = default
|
37
|
+
end
|
38
|
+
else
|
39
|
+
@cache[path][:data] = File.read(path)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
if block_given? && !expected_type.nil?
|
44
|
+
unless @cache[path][:data].is_a?(expected_type)
|
45
|
+
Hiera.debug("Data retrieved from %s is not a %s, setting defaults" % [path, expected_type])
|
46
|
+
@cache[path][:data] = default
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
@cache[path][:data]
|
51
|
+
end
|
52
|
+
|
53
|
+
def stale?(path)
|
54
|
+
meta = path_metadata(path)
|
55
|
+
|
56
|
+
@cache[path] ||= {:data => nil, :meta => nil}
|
57
|
+
|
58
|
+
if @cache[path][:meta] == meta
|
59
|
+
return false
|
60
|
+
else
|
61
|
+
@cache[path][:meta] = meta
|
62
|
+
return true
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# This is based on the old caching in the YAML backend and has a
|
67
|
+
# resolution of 1 second, changes made within the same second of
|
68
|
+
# a previous read will be ignored
|
69
|
+
def path_metadata(path)
|
70
|
+
stat = File.stat(path)
|
71
|
+
{:inode => stat.ino, :mtime => stat.mtime, :size => stat.size}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/hiera/puppet_logger.rb
CHANGED
@@ -0,0 +1,31 @@
|
|
1
|
+
# Allow for safe recursive lookup of values during variable interpolation.
|
2
|
+
#
|
3
|
+
# @api private
|
4
|
+
class Hiera::RecursiveLookup
|
5
|
+
def initialize(scope, extra_data)
|
6
|
+
@seen = []
|
7
|
+
@scope = scope
|
8
|
+
@extra_data = extra_data
|
9
|
+
end
|
10
|
+
|
11
|
+
def lookup(name, &block)
|
12
|
+
if @seen.include?(name)
|
13
|
+
raise Exception, "Interpolation loop detected in [#{@seen.join(', ')}]"
|
14
|
+
end
|
15
|
+
@seen.push(name)
|
16
|
+
ret = yield(current_value)
|
17
|
+
@seen.pop
|
18
|
+
ret
|
19
|
+
end
|
20
|
+
|
21
|
+
def current_value
|
22
|
+
name = @seen.last
|
23
|
+
|
24
|
+
scope_val = @scope[name]
|
25
|
+
if scope_val.nil? || scope_val == :undefined
|
26
|
+
@extra_data[name]
|
27
|
+
else
|
28
|
+
scope_val
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -8,7 +8,8 @@ class Hiera
|
|
8
8
|
Hiera.stubs(:debug)
|
9
9
|
Hiera.stubs(:warn)
|
10
10
|
Hiera::Backend.stubs(:empty_answer).returns(nil)
|
11
|
-
@
|
11
|
+
@cache = mock
|
12
|
+
@backend = Json_backend.new(@cache)
|
12
13
|
end
|
13
14
|
|
14
15
|
describe "#initialize" do
|
@@ -30,13 +31,9 @@ class Hiera
|
|
30
31
|
it "should retain the data types found in data files" do
|
31
32
|
Backend.expects(:datasources).yields("one").times(3)
|
32
33
|
Backend.expects(:datafile).with(:json, {}, "one", "json").returns("/nonexisting/one.json").times(3)
|
33
|
-
File.
|
34
|
-
"boolval":true,
|
35
|
-
"numericval":1}').times(3)
|
34
|
+
File.stubs(:exist?).with("/nonexisting/one.json").returns(true)
|
36
35
|
|
37
|
-
|
38
|
-
Backend.stubs(:parse_answer).with(true, {}).returns(true)
|
39
|
-
Backend.stubs(:parse_answer).with(1, {}).returns(1)
|
36
|
+
@cache.expects(:read).with("/nonexisting/one.json", Hash, {}).returns({"stringval" => "string", "boolval" => true, "numericval" => 1}).times(3)
|
40
37
|
|
41
38
|
@backend.lookup("stringval", {}, nil, :priority).should == "string"
|
42
39
|
@backend.lookup("boolval", {}, nil, :priority).should == true
|
@@ -45,13 +42,12 @@ class Hiera
|
|
45
42
|
|
46
43
|
it "should pick data earliest source that has it for priority searches" do
|
47
44
|
scope = {"rspec" => "test"}
|
48
|
-
Backend.stubs(:parse_answer).with('answer', scope).returns("answer")
|
49
|
-
Backend.stubs(:parse_answer).with('test_%{rspec}', scope).returns("test_test")
|
50
45
|
Backend.expects(:datasources).multiple_yields(["one"], ["two"])
|
51
46
|
Backend.expects(:datafile).with(:json, scope, "one", "json").returns("/nonexisting/one.json")
|
52
|
-
Backend.expects(:datafile).with(:json, scope, "two", "json").
|
53
|
-
|
54
|
-
|
47
|
+
Backend.expects(:datafile).with(:json, scope, "two", "json").never
|
48
|
+
|
49
|
+
File.stubs(:exist?).with("/nonexisting/one.json").returns(true)
|
50
|
+
@cache.expects(:read).with("/nonexisting/one.json", Hash, {}).returns({"key" => "test_%{rspec}"})
|
55
51
|
|
56
52
|
@backend.lookup("key", scope, nil, :priority).should == "test_test"
|
57
53
|
end
|
@@ -64,11 +60,11 @@ class Hiera
|
|
64
60
|
|
65
61
|
Backend.expects(:datasources).multiple_yields(["one"], ["two"])
|
66
62
|
|
67
|
-
File.expects(:
|
68
|
-
File.expects(:
|
63
|
+
File.expects(:exist?).with("/nonexisting/one.json").returns(true)
|
64
|
+
File.expects(:exist?).with("/nonexisting/two.json").returns(true)
|
69
65
|
|
70
|
-
|
71
|
-
|
66
|
+
@cache.expects(:read).with("/nonexisting/one.json", Hash, {}).returns({"key" => "answer"})
|
67
|
+
@cache.expects(:read).with("/nonexisting/two.json", Hash, {}).returns({"key" => "answer"})
|
72
68
|
|
73
69
|
@backend.lookup("key", {}, nil, :array).should == ["answer", "answer"]
|
74
70
|
end
|
@@ -78,8 +74,8 @@ class Hiera
|
|
78
74
|
Backend.expects(:datasources).yields("one")
|
79
75
|
Backend.expects(:datafile).with(:json, {"rspec" => "test"}, "one", "json").returns("/nonexisting/one.json")
|
80
76
|
|
81
|
-
File.expects(:
|
82
|
-
|
77
|
+
File.expects(:exist?).with("/nonexisting/one.json").returns(true)
|
78
|
+
@cache.expects(:read).with("/nonexisting/one.json", Hash, {}).returns({"key" => "test_%{rspec}"})
|
83
79
|
|
84
80
|
@backend.lookup("key", {"rspec" => "test"}, nil, :priority).should == "test_test"
|
85
81
|
end
|