Skip to content

Commit 1ff2c16

Browse files
committed
(FACT-2786) Fix fact caching if fact is defined in multiple groups
1 parent b496e04 commit 1ff2c16

7 files changed

Lines changed: 155 additions & 36 deletions

File tree

acceptance/tests/custom_facts/cached_custom_fact.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373

7474
step 'should read from the cached file for a custom fact that has been cached' do
7575
on(agent, facter("#{custom_fact_name} --debug", environment: env)) do |facter_result|
76-
assert_match(/Loading cached custom facts from file ".+"|loading cached values for cached-custom-facts facts/, facter_result.stderr,
76+
assert_match(/Loading cached custom facts from file ".+"|loading cached values for random_custom_fact facts/, facter_result.stderr,
7777
'Expected debug message to state that cached custom facts are read from file')
7878
end
7979
end

acceptance/tests/custom_facts/weighted_cached_custom_facts.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@
8585

8686
step 'should read from the cached file for a custom fact that has been cached' do
8787
on(agent, facter("#{duplicate_custom_fact_name} --debug", environment: env)) do |facter_result|
88-
assert_match(/Loading cached custom facts from file ".+"|loading cached values for cached-custom-facts facts/, facter_result.stderr,
88+
assert_match(/Loading cached custom facts from file ".+"|loading cached values for random_custom_fact facts/, facter_result.stderr,
8989
'Expected debug message to state that cached custom facts are read from file')
9090
end
9191
end
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# This test verifies that individual facts can be cached
2+
test_name "ttls config that contains fact name caches individual facts" do
3+
tag 'risk:high'
4+
5+
require 'facter/acceptance/user_fact_utils'
6+
extend Facter::Acceptance::UserFactUtils
7+
8+
# This fact must be resolvable on ALL platforms
9+
# Do NOT use the 'kernel' fact as it is used to configure the tests
10+
cached_fact_name = 'system_uptime.days'
11+
cached_fact_value = "CACHED_FACT_VALUE"
12+
cached_fact_content = <<EOM
13+
{
14+
"#{cached_fact_name}": "#{cached_fact_value}"
15+
}
16+
EOM
17+
18+
config = <<EOM
19+
facts : {
20+
ttls : [
21+
{ "#{cached_fact_name}" : 30 days }
22+
]
23+
}
24+
EOM
25+
26+
agents.each do |agent|
27+
step "Agent #{agent}: create cahce file with indivitual fact" do
28+
config_dir = get_default_fact_dir(agent['platform'], on(agent, facter('kernelmajversion')).stdout.chomp.to_f)
29+
config_file = File.join(config_dir, "facter.conf")
30+
cached_facts_dir = get_cached_facts_dir(agent['platform'], on(agent, facter('kernelmajversion')).stdout.chomp.to_f)
31+
32+
cached_fact_file = File.join(cached_facts_dir, cached_factname)
33+
34+
# Setup facter conf
35+
agent.mkdir_p(config_dir)
36+
create_remote_file(agent, config_file, config)
37+
38+
teardown do
39+
agent.rm_rf(config_dir)
40+
agent.rm_rf(cached_facts_dir)
41+
end
42+
43+
step "should create a JSON file for a fact that is to be cached" do
44+
agent.rm_rf(cached_facts_dir)
45+
on(agent, facter("--debug")) do |facter_output|
46+
assert_match(/caching values for .+ facts/, facter_output.stderr, "Expected debug message to state that values will be cached")
47+
end
48+
cat_output = agent.cat(cached_fact_file)
49+
assert_match(/#{cached_fact_name}/, cat_output.strip, "Expected cached fact file to contain fact information")
50+
end
51+
52+
step "should read from a cached JSON file for a fact that has been cached" do
53+
agent.mkdir_p(cached_facts_dir)
54+
create_remote_file(agent, cached_fact_file, cached_fact_content)
55+
56+
on(agent, facter("#{cached_fact_name} --debug")) do
57+
assert_match(/loading cached values for .+ facts/, stderr, "Expected debug message to state that values are read from cache")
58+
assert_match(/#{cached_fact_value}/, stdout, "Expected fact to match the cached fact file")
59+
end
60+
end
61+
end
62+
end
63+
end

lib/facter/custom_facts/util/directory_loader.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def load_directory_entries(_collection)
5959
basename = File.basename(file)
6060
next if file_blocked?(basename)
6161

62-
if facts.find { |f| f.name == basename } && cm.group_cached?(basename)
62+
if facts.find { |f| f.name == basename } && cm.fact_cache_enabled?(basename)
6363
Facter.log_exception(Exception.new("Caching is enabled for group \"#{basename}\" while "\
6464
'there are at least two external facts files with the same filename'))
6565
else

lib/facter/framework/config/fact_groups.rb

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
module Facter
44
class FactGroups
5-
attr_reader :groups, :block_list
5+
attr_reader :groups, :block_list, :facts_ttls
66

77
@groups_ttls = []
88

@@ -14,6 +14,10 @@ def initialize(group_list_path = nil)
1414
@groups ||= File.readable?(@groups_file_path) ? Hocon.load(@groups_file_path) : {}
1515
load_groups
1616
load_groups_from_options
17+
load_facts_ttls
18+
19+
# Reverse sort facts so that children have precedence when caching. eg: os.macosx vs os
20+
@facts_ttls = @facts_ttls.sort.reverse.to_h
1721
end
1822

1923
# Breakes down blocked groups in blocked facts
@@ -31,6 +35,9 @@ def blocked_facts
3135

3236
# Get the group name a fact is part of
3337
def get_fact_group(fact_name)
38+
fact = get_fact(fact_name)
39+
return fact[:group] if fact
40+
3441
@groups.detect { |k, v| break k if Array(v).find { |f| fact_name =~ /^#{f}.*/ } }
3542
end
3643

@@ -41,6 +48,15 @@ def get_group_ttls(group_name)
4148
ttls_to_seconds(ttls[group_name])
4249
end
4350

51+
def get_fact(fact_name)
52+
return @facts_ttls[fact_name] if @facts_ttls[fact_name]
53+
54+
result = @facts_ttls.select { |name, fact| break fact if fact_name =~ /^#{name}\..*/ }
55+
return nil if result == {}
56+
57+
result
58+
end
59+
4460
private
4561

4662
def load_groups_from_options
@@ -55,10 +71,30 @@ def load_groups_from_options
5571
end
5672
end
5773

74+
def load_facts_ttls
75+
@facts_ttls ||= {}
76+
return if @groups_ttls == []
77+
78+
@groups_ttls.reduce(:merge).each do |group, ttls|
79+
ttls = ttls_to_seconds(ttls)
80+
if @groups[group]
81+
@groups[group].each do |fact|
82+
if @facts_ttls[fact]
83+
@facts_ttls[fact] = { ttls: ttls, group: group } if @facts_ttls[fact][:ttls] < ttls
84+
else
85+
@facts_ttls[fact] = { ttls: ttls, group: group }
86+
end
87+
end
88+
else
89+
@facts_ttls[group] = { ttls: ttls, group: group }
90+
end
91+
end
92+
end
93+
5894
def load_groups
5995
config = ConfigReader.init(Options[:config])
6096
@block_list = config.block_list || {}
61-
@groups_ttls = config.ttls || {}
97+
@groups_ttls = config.ttls || []
6298
@groups.merge!(config.fact_groups) if config.fact_groups
6399
end
64100

lib/facter/framework/core/cache_manager.rb

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -40,31 +40,42 @@ def cache_facts(resolved_facts)
4040
end
4141
end
4242

43-
def group_cached?(group_name)
44-
cached = @fact_groups.get_group_ttls(group_name) ? true : false
45-
delete_cache(group_name) unless cached
43+
def fact_cache_enabled?(fact_name)
44+
fact = @fact_groups.get_fact(fact_name)
45+
cached = if fact
46+
!fact[:ttls].nil?
47+
else
48+
false
49+
end
50+
51+
fact_group = @fact_groups.get_fact_group(fact_name)
52+
delete_cache(fact_group) if fact_group && !cached
4653
cached
4754
end
4855

4956
private
5057

5158
def resolve_fact(searched_fact)
52-
group_name = if searched_fact.file
53-
searched_fact.name
54-
else
55-
@fact_groups.get_fact_group(searched_fact.name)
56-
end
59+
return unless fact_cache_enabled?(searched_fact.name)
5760

58-
return unless group_name
61+
fact = if searched_fact.file
62+
@fact_groups.get_fact(File.basename(searched_fact.file))
63+
else
64+
@fact_groups.get_fact(searched_fact.name)
65+
end
5966

60-
return unless group_cached?(group_name)
67+
return unless fact
6168

62-
return unless check_ttls?(group_name)
69+
return unless check_ttls?(fact[:group], fact[:ttls])
6370

64-
data = read_group_json(group_name)
71+
read_fact(searched_fact, fact[:group])
72+
end
73+
74+
def read_fact(searched_fact, fact_group)
75+
data = read_group_json(fact_group)
6576
return unless data
6677

67-
@log.debug("loading cached values for #{group_name} facts")
78+
@log.debug("loading cached values for #{searched_fact.name} facts")
6879

6980
create_facts(searched_fact, data)
7081
end
@@ -93,7 +104,7 @@ def cache_fact(fact)
93104
end
94105
return if !group_name || fact.value.nil?
95106

96-
return unless group_cached?(group_name)
107+
return unless fact_cache_enabled?(fact.name)
97108

98109
@groups[group_name] ||= {}
99110
@groups[group_name][fact.name] = fact.value
@@ -106,10 +117,12 @@ def write_cache
106117
end
107118

108119
@groups.each do |group_name, data|
109-
next unless check_ttls?(group_name)
120+
next unless check_ttls?(group_name, @fact_groups.get_group_ttls(group_name))
110121

111-
@log.debug("caching values for #{group_name} facts")
112122
cache_file_name = File.join(@cache_dir, group_name)
123+
next if File.readable?(cache_file_name)
124+
125+
@log.debug("caching values for #{group_name} facts")
113126
File.write(cache_file_name, JSON.pretty_generate(data))
114127
end
115128
end
@@ -128,8 +141,7 @@ def read_group_json(group_name)
128141
@groups[group_name] = data
129142
end
130143

131-
def check_ttls?(group_name)
132-
ttls = @fact_groups.get_group_ttls(group_name)
144+
def check_ttls?(group_name, ttls)
133145
return false unless ttls
134146

135147
cache_file_name = File.join(@cache_dir, group_name)

spec/facter/cache_manager_spec.rb

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
let(:group_name) { 'operating system' }
2626
let(:cache_file_name) { File.join(cache_dir, group_name) }
2727
let(:fact_groups) { instance_spy(Facter::FactGroups) }
28+
let(:os_fact) { { ttls: 60, group: 'operating system' } }
29+
let(:external_fact) { { ttls: 60, group: 'ext_file.txt' } }
2830

2931
before do
3032
allow(LegacyFacter::Util::Config).to receive(:facts_cache_dir).and_return(cache_dir)
@@ -72,15 +74,18 @@
7274
allow(File).to receive(:directory?).with(cache_dir).and_return(true)
7375
allow(fact_groups).to receive(:get_fact_group).with('os').and_return(group_name)
7476
allow(fact_groups).to receive(:get_fact_group).with('my_custom_fact').and_return(nil)
77+
allow(fact_groups).to receive(:get_fact_group).with('ext_file.txt').and_return(nil)
7578
allow(fact_groups).to receive(:get_group_ttls).with('ext_file.txt').and_return(nil)
79+
allow(fact_groups).to receive(:get_fact).with('ext_file.txt').and_return(nil)
80+
allow(fact_groups).to receive(:get_fact).with('my_custom_fact').and_return(nil)
7681
allow(File).to receive(:readable?).with(cache_file_name).and_return(true)
7782
allow(File).to receive(:mtime).with(cache_file_name).and_return(Time.now)
7883
allow(Facter::Util::FileHelper).to receive(:safe_read).with(cache_file_name).and_return(cached_core_fact)
7984
allow(Facter::Options).to receive(:[]).with(:cache).and_return(true)
8085
end
8186

8287
it 'returns cached fact' do
83-
allow(fact_groups).to receive(:get_group_ttls).with(group_name).and_return(1000)
88+
allow(fact_groups).to receive(:get_fact).with('os').and_return(os_fact)
8489
allow(File).to receive(:readable?).with(cache_file_name).and_return(true)
8590
allow(File).to receive(:readable?).with(File.join(cache_dir, 'ext_file.txt')).and_return(false)
8691

@@ -91,7 +96,7 @@
9196
end
9297

9398
it 'returns searched fact' do
94-
allow(fact_groups).to receive(:get_group_ttls).with(group_name).and_return(1000)
99+
allow(fact_groups).to receive(:get_fact).with('os').and_return(os_fact)
95100
allow(File).to receive(:readable?).with(cache_file_name).and_return(true)
96101
allow(File).to receive(:readable?).with(File.join(cache_dir, 'ext_file.txt')).and_return(false)
97102

@@ -103,18 +108,20 @@
103108
end
104109

105110
it 'deletes cache file' do
106-
allow(fact_groups).to receive(:get_group_ttls).with(group_name).and_return(nil)
111+
allow(fact_groups).to receive(:get_fact).with('os').and_return(nil)
107112
allow(File).to receive(:readable?).with(cache_file_name).and_return(true)
108113
allow(File).to receive(:delete).with(cache_file_name)
114+
allow(fact_groups).to receive(:get_fact_group).with('os').and_return(group_name)
109115
allow(File).to receive(:readable?).with(File.join(cache_dir, 'ext_file.txt')).and_return(false)
110116

111117
cache_manager.resolve_facts(searched_facts)
112118
expect(File).to have_received(:delete).with(cache_file_name)
113119
end
114120

115121
it 'returns cached external facts' do
116-
allow(fact_groups).to receive(:get_group_ttls).with('ext_file.txt').and_return(1000)
117-
allow(fact_groups).to receive(:get_group_ttls).with(group_name).and_return(nil)
122+
allow(fact_groups).to receive(:get_fact).with('os').and_return(nil)
123+
allow(fact_groups).to receive(:get_fact).with('my_custom_fact').and_return(nil)
124+
allow(fact_groups).to receive(:get_fact).with('ext_file.txt').and_return(external_fact)
118125
allow(File).to receive(:readable?).with(cache_file_name).and_return(false)
119126
allow(Facter::Util::FileHelper).to receive(:safe_read).with(File.join(cache_dir, 'ext_file.txt'))
120127
.and_return(cached_external_fact)
@@ -135,7 +142,7 @@
135142
before do
136143
allow(File).to receive(:directory?).with(cache_dir).and_return(true)
137144
allow(File).to receive(:readable?).with(cache_file_name).and_return(false)
138-
allow(fact_groups).to receive(:get_group_ttls).with(group_name).and_return(nil)
145+
allow(fact_groups).to receive(:get_fact).with('os').and_return(nil)
139146
allow(fact_groups).to receive(:get_fact_group).with('os').and_return(group_name)
140147
allow(File).to receive(:write).with(cache_file_name, cached_core_fact)
141148
allow(Facter::Options).to receive(:[]).with(:cache).and_return(true)
@@ -150,10 +157,10 @@
150157
context 'with cache group' do
151158
before do
152159
allow(File).to receive(:directory?).with(cache_dir).and_return(true)
160+
allow(fact_groups).to receive(:get_fact).with('os').and_return(os_fact)
153161
allow(fact_groups).to receive(:get_fact_group).with('os').and_return(group_name)
154162
allow(fact_groups).to receive(:get_fact_group).with('my_custom_fact').and_return(nil)
155163
allow(fact_groups).to receive(:get_fact_group).with('my_external_fact').and_return(nil)
156-
allow(fact_groups).to receive(:get_group_ttls).with(group_name).and_return(1000)
157164
allow(File).to receive(:readable?).with(cache_file_name).and_return(false)
158165
allow(File).to receive(:write).with(cache_file_name, cached_core_fact)
159166
allow(Facter::Options).to receive(:[]).with(:cache).and_return(true)
@@ -166,35 +173,36 @@
166173
end
167174
end
168175

169-
describe '#group_cached?' do
176+
describe '#fact_cache_enabled?' do
170177
context 'with ttls' do
171178
before do
172-
allow(fact_groups).to receive(:get_group_ttls).with(group_name).and_return(1000)
179+
allow(fact_groups).to receive(:get_fact).with('os').and_return(os_fact)
173180
allow(File).to receive(:readable?).with(cache_file_name).and_return(false)
174181
end
175182

176183
it 'returns true' do
177-
result = cache_manager.group_cached?(group_name)
184+
result = cache_manager.fact_cache_enabled?('os')
178185
expect(result).to be true
179186
end
180187
end
181188

182189
context 'without ttls' do
183190
before do
184-
allow(fact_groups).to receive(:get_group_ttls).with(group_name).and_return(nil)
191+
allow(fact_groups).to receive(:get_fact).with('os').and_return(nil)
192+
allow(fact_groups).to receive(:get_fact_group).with('os').and_return(group_name)
185193
allow(Facter::Options).to receive(:[]).with(:cache).and_return(true)
186194
allow(File).to receive(:delete).with(cache_file_name)
187195
end
188196

189197
it 'returns false' do
190198
allow(File).to receive(:readable?).with(cache_file_name).and_return(false)
191-
result = cache_manager.group_cached?(group_name)
199+
result = cache_manager.fact_cache_enabled?('os')
192200
expect(result).to be false
193201
end
194202

195203
it 'deletes invalid cache file' do
196204
allow(File).to receive(:readable?).with(cache_file_name).and_return(true)
197-
cache_manager.group_cached?(group_name)
205+
cache_manager.fact_cache_enabled?('os')
198206
expect(File).to have_received(:delete).with(cache_file_name)
199207
end
200208
end

0 commit comments

Comments
 (0)