Skip to content

Commit fa3f0f4

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

8 files changed

Lines changed: 210 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: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# This test verifies that individual facts can be cached
2+
test_name "ttls config with fact in multiple groups should not cache fact twice" 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 = 'os.name'
11+
first_fact_group = 'first'
12+
second_fact_group = 'second'
13+
14+
config = <<EOM
15+
facts : {
16+
ttls : [
17+
{ "#{first_fact_group}" : 30 days },
18+
{ "#{second_fact_group}" : 1 days },
19+
]
20+
}
21+
fact-groups : {
22+
#{first_fact_group}: [#{cached_fact_name}],
23+
#{second_fact_group}: ["os"],
24+
}
25+
EOM
26+
27+
agents.each do |agent|
28+
step "Agent #{agent}: create cache file with individual fact" do
29+
config_dir = get_default_fact_dir(agent['platform'], on(agent, facter('kernelmajversion')).stdout.chomp.to_f)
30+
config_file = File.join(config_dir, "facter.conf")
31+
cached_facts_dir = get_cached_facts_dir(agent['platform'], on(agent, facter('kernelmajversion')).stdout.chomp.to_f)
32+
33+
first_cached_fact_file = File.join(cached_facts_dir, first_fact_group)
34+
second_cached_fact_file = File.join(cached_facts_dir, second_fact_group)
35+
36+
# Setup facter conf
37+
agent.mkdir_p(config_dir)
38+
create_remote_file(agent, config_file, config)
39+
40+
teardown do
41+
agent.rm_rf(config_dir)
42+
agent.rm_rf(cached_facts_dir)
43+
end
44+
45+
step "should create a JSON file for a fact that is to be cached" do
46+
agent.rm_rf(cached_facts_dir)
47+
on(agent, facter("--debug")) do |facter_output|
48+
assert_match(/caching values for .+ facts/, facter_output.stderr, "Expected debug message to state that values will be cached")
49+
end
50+
first_cat_output = agent.cat(first_cached_fact_file)
51+
assert_match(/#{cached_fact_name}/, first_cat_output.strip, "Expected cached fact file to contain fact information")
52+
second_cat_output = agent.cat(second_cached_fact_file)
53+
assert_not_match(/#{cached_fact_name}/, second_cat_output.strip, "Expected cached fact file to not contain fact information")
54+
end
55+
end
56+
end
57+
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 cache file with individual 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_fact_name)
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: 36 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,28 @@ 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] && @facts_ttls[fact][:ttls] < ttls) || @facts_ttls[fact].nil?
83+
@facts_ttls[fact] = { ttls: ttls, group: group }
84+
end
85+
end
86+
else
87+
@facts_ttls[group] = { ttls: ttls, group: group }
88+
end
89+
end
90+
end
91+
5892
def load_groups
5993
config = ConfigReader.init(Options[:config])
6094
@block_list = config.block_list || {}
61-
@groups_ttls = config.ttls || {}
95+
@groups_ttls = config.ttls || []
6296
@groups.merge!(config.fact_groups) if config.fact_groups
6397
end
6498

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)

0 commit comments

Comments
 (0)