Skip to content

Commit 88905a5

Browse files
authored
Merge pull request #8784 from luchihoratiu/PUP-11204
(PUP-11204) Allow `puppet lookup --facts` to receive any filename
2 parents 45def53 + 430ffa3 commit 88905a5

File tree

6 files changed

+312
-48
lines changed

6 files changed

+312
-48
lines changed

lib/puppet/application/lookup.rb

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,7 @@ class Puppet::Application::Lookup < Puppet::Application
5454
end
5555

5656
option('--facts FACT_FILE') do |arg|
57-
if %w{.yaml .yml .json}.include?(arg.match(/\.[^.]*$/)[0])
58-
options[:fact_file] = arg
59-
else
60-
raise _("The --fact file only accepts yaml and json files.\n%{run_help}") % { run_help: RUN_HELP }
61-
end
57+
options[:fact_file] = arg
6258
end
6359

6460
def app_defaults
@@ -356,14 +352,17 @@ def generate_scope
356352
fact_file = options[:fact_file]
357353

358354
if fact_file
359-
if fact_file.end_with?("json")
360-
given_facts = Puppet::Util::Json.load(Puppet::FileSystem.read(fact_file, :encoding => 'utf-8'))
361-
else
355+
if fact_file.end_with?('.json')
356+
given_facts = Puppet::Util::Json.load_file(fact_file)
357+
elsif fact_file.end_with?('.yml', '.yaml')
362358
given_facts = Puppet::Util::Yaml.safe_load_file(fact_file)
359+
else
360+
given_facts = Puppet::Util::Json.load_file_if_valid(fact_file)
361+
given_facts = Puppet::Util::Yaml.safe_load_file_if_valid(fact_file) unless given_facts
363362
end
364363

365364
unless given_facts.instance_of?(Hash)
366-
raise _("Incorrect formatted data in %{fact_file} given via the --facts flag") % { fact_file: fact_file }
365+
raise _("Incorrectly formatted data in %{fact_file} given via the --facts flag (only accepts yaml and json files)") % { fact_file: fact_file }
367366
end
368367
node.add_extra_facts(given_facts)
369368
end

lib/puppet/util/json.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,23 @@ def self.build(original_exception, data)
2626
require 'json'
2727
end
2828

29+
# Load the content from a file as JSON if
30+
# contents are in valid format. This method does not
31+
# raise error but returns `nil` when invalid file is
32+
# given.
33+
def self.load_file_if_valid(filename, options = {})
34+
load_file(filename, options)
35+
rescue Puppet::Util::Json::ParseError, ArgumentError, Errno::ENOENT => detail
36+
Puppet.debug("Could not retrieve JSON content from '#{filename}': #{detail.message}")
37+
nil
38+
end
39+
40+
# Load the content from a file as JSON.
41+
def self.load_file(filename, options = {})
42+
json = Puppet::FileSystem.read(filename, :encoding => 'utf-8')
43+
load(json, options)
44+
end
45+
2946
# These methods do similar processing to the fallback implemented by MultiJson
3047
# when using the built-in JSON backend, to ensure consistent behavior
3148
# whether or not MultiJson can be loaded.

lib/puppet/util/yaml.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ def self.safe_load_file(filename, allowed_classes = [])
4242
safe_load(yaml, allowed_classes, filename)
4343
end
4444

45+
# Safely load the content from a file as YAML if
46+
# contents are in valid format. This method does not
47+
# raise error but returns `nil` when invalid file is
48+
# given.
49+
def self.safe_load_file_if_valid(filename, allowed_classes = [])
50+
safe_load_file(filename, allowed_classes)
51+
rescue YamlLoadError, ArgumentError, Errno::ENOENT => detail
52+
Puppet.debug("Could not retrieve YAML content from '#{filename}': #{detail.message}")
53+
nil
54+
end
55+
4556
# @deprecated Use {#safe_load_file} instead.
4657
def self.load_file(filename, default_value = false, strip_classes = false)
4758
Puppet.deprecation_warning(_("Puppet::Util::Yaml.load_file is deprecated. Use safe_load_file instead."))

spec/unit/application/lookup_spec.rb

Lines changed: 96 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -518,34 +518,120 @@ def run_lookup(lookup)
518518
end
519519

520520
it 'receives extra facts in top scope' do
521-
file_path = tmpdir('lookup_spec')
522-
filename = File.join(file_path, "facts.yaml")
523-
File.open(filename, "w+") { |f| f.write(<<-YAML.unindent) }
521+
file_path = file_containing('facts.yaml', <<~CONTENT)
524522
---
525523
cx: ' C from facts'
526-
YAML
524+
CONTENT
527525

528526
lookup.options[:node] = node
529-
lookup.options[:fact_file] = filename
527+
lookup.options[:fact_file] = file_path
530528
lookup.options[:render_as] = :s
531529
allow(lookup.command_line).to receive(:args).and_return(['c'])
532530
expect(run_lookup(lookup)).to eql("This is C from facts")
533531
end
534532

535533
it 'receives extra facts in the facts hash' do
536-
file_path = tmpdir('lookup_spec')
537-
filename = File.join(file_path, "facts.yaml")
538-
File.open(filename, "w+") { |f| f.write(<<-YAML.unindent) }
534+
file_path = file_containing('facts.yaml', <<~CONTENT)
539535
---
540536
cx: ' G from facts'
541-
YAML
537+
CONTENT
542538

543539
lookup.options[:node] = node
544-
lookup.options[:fact_file] = filename
540+
lookup.options[:fact_file] = file_path
545541
lookup.options[:render_as] = :s
546542
allow(lookup.command_line).to receive(:args).and_return(['g'])
547543
expect(run_lookup(lookup)).to eql("This is G from facts in facts hash")
548544
end
545+
546+
describe 'when retrieving given facts' do
547+
before do
548+
lookup.options[:node] = node
549+
allow(lookup.command_line).to receive(:args).and_return(['g'])
550+
end
551+
552+
it 'loads files with yaml extension as yaml on first try' do
553+
file_path = file_containing('facts.yaml', <<~CONTENT)
554+
---
555+
cx: ' G from facts'
556+
CONTENT
557+
lookup.options[:fact_file] = file_path
558+
559+
expect(Puppet::Util::Yaml).to receive(:safe_load_file).with(file_path, []).and_call_original
560+
run_lookup(lookup)
561+
end
562+
563+
it 'loads files with yml extension as yaml on first try' do
564+
file_path = file_containing('facts.yml', <<~CONTENT)
565+
---
566+
cx: ' G from facts'
567+
CONTENT
568+
lookup.options[:fact_file] = file_path
569+
570+
expect(Puppet::Util::Yaml).to receive(:safe_load_file).with(file_path, []).and_call_original
571+
run_lookup(lookup)
572+
end
573+
574+
it 'loads files with json extension as json on first try' do
575+
file_path = file_containing('facts.json', <<~CONTENT)
576+
{
577+
"cx": " G from facts"
578+
}
579+
CONTENT
580+
lookup.options[:fact_file] = file_path
581+
582+
expect(Puppet::Util::Json).to receive(:load_file).with(file_path, {}).and_call_original
583+
run_lookup(lookup)
584+
end
585+
586+
it 'detects file format accordingly even with random file extension' do
587+
file_path = file_containing('facts.txt', <<~CONTENT)
588+
{
589+
"cx": " G from facts"
590+
}
591+
CONTENT
592+
lookup.options[:fact_file] = file_path
593+
594+
expect(Puppet::Util::Json).to receive(:load_file_if_valid).with(file_path).and_call_original
595+
expect(Puppet::Util::Yaml).not_to receive(:safe_load_file_if_valid).with(file_path)
596+
run_lookup(lookup)
597+
end
598+
599+
it 'detects file without extension as json due to valid contents' do
600+
file_path = file_containing('facts', <<~CONTENT)
601+
{
602+
"cx": " G from facts"
603+
}
604+
CONTENT
605+
lookup.options[:fact_file] = file_path
606+
607+
expect(Puppet::Util::Json).to receive(:load_file_if_valid).with(file_path).and_call_original
608+
expect(Puppet::Util::Yaml).not_to receive(:safe_load_file_if_valid).with(file_path)
609+
run_lookup(lookup)
610+
end
611+
612+
it 'detects file without extension as yaml due to valid contents' do
613+
file_path = file_containing('facts', <<~CONTENT)
614+
---
615+
cx: ' G from facts'
616+
CONTENT
617+
lookup.options[:fact_file] = file_path
618+
619+
expect(Puppet::Util::Json.load_file_if_valid(file_path)).to be_nil
620+
expect(Puppet::Util::Yaml).to receive(:safe_load_file_if_valid).with(file_path).and_call_original
621+
run_lookup(lookup)
622+
end
623+
624+
it 'raises error due to invalid contents' do
625+
file_path = file_containing('facts.yml', <<~CONTENT)
626+
INVALID CONTENT
627+
CONTENT
628+
lookup.options[:fact_file] = file_path
629+
630+
expect {
631+
lookup.run_command
632+
}.to raise_error(/Incorrectly formatted data in .+ given via the --facts flag \(only accepts yaml and json files\)/)
633+
end
634+
end
549635
end
550636

551637
context 'using a puppet function as data provider' do

spec/unit/util/json_spec.rb

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# coding: utf-8
2+
require 'spec_helper'
3+
require 'puppet/util/json'
4+
5+
describe Puppet::Util::Json do
6+
include PuppetSpec::Files
7+
8+
shared_examples_for 'json file loader' do |load_method|
9+
it 'reads a JSON file from disk' do
10+
file_path = file_containing('input', JSON.dump({ "my" => "data" }))
11+
12+
expect(load_method.call(file_path)).to eq({ "my" => "data" })
13+
end
14+
15+
it 'reads JSON as UTF-8' do
16+
file_path = file_containing('input', JSON.dump({ "my" => "𠜎" }))
17+
18+
expect(load_method.call(file_path)).to eq({ "my" => "𠜎" })
19+
end
20+
end
21+
22+
context "#load" do
23+
it 'raises an error if JSON is invalid' do
24+
expect {
25+
Puppet::Util::Json.load('{ invalid')
26+
}.to raise_error(Puppet::Util::Json::ParseError, /unexpected token at '{ invalid'/)
27+
end
28+
29+
it 'raises an error if the content is empty' do
30+
expect {
31+
Puppet::Util::Json.load('')
32+
}.to raise_error(Puppet::Util::Json::ParseError)
33+
end
34+
35+
it 'loads true' do
36+
expect(Puppet::Util::Json.load('true')).to eq(true)
37+
end
38+
39+
it 'loads false' do
40+
expect(Puppet::Util::Json.load('false')).to eq(false)
41+
end
42+
43+
it 'loads a numeric' do
44+
expect(Puppet::Util::Json.load('42')).to eq(42)
45+
end
46+
47+
it 'loads a string' do
48+
expect(Puppet::Util::Json.load('"puppet"')).to eq('puppet')
49+
end
50+
51+
it 'loads an array' do
52+
expect(Puppet::Util::Json.load(<<~JSON)).to eq([1, 2])
53+
[1, 2]
54+
JSON
55+
end
56+
57+
it 'loads a hash' do
58+
expect(Puppet::Util::Json.load(<<~JSON)).to eq('a' => 1, 'b' => 2)
59+
{
60+
"a": 1,
61+
"b": 2
62+
}
63+
JSON
64+
end
65+
end
66+
67+
context "load_file_if_valid" do
68+
before do
69+
Puppet[:log_level] = 'debug'
70+
end
71+
72+
it_should_behave_like 'json file loader', Puppet::Util::Json.method(:load_file_if_valid)
73+
74+
it 'returns nil when the file is invalid JSON and debug logs about it' do
75+
file_path = file_containing('input', '{ invalid')
76+
expect(Puppet).to receive(:debug)
77+
.with(/Could not retrieve JSON content .+: unexpected token at '{ invalid'/).and_call_original
78+
79+
expect(Puppet::Util::Json.load_file_if_valid(file_path)).to eql(nil)
80+
end
81+
82+
it 'returns nil when the filename is illegal and debug logs about it' do
83+
expect(Puppet).to receive(:debug)
84+
.with(/Could not retrieve JSON content .+: pathname contains null byte/).and_call_original
85+
86+
expect(Puppet::Util::Json.load_file_if_valid("not\0allowed")).to eql(nil)
87+
end
88+
89+
it 'returns nil when the file does not exist and debug logs about it' do
90+
expect(Puppet).to receive(:debug)
91+
.with(/Could not retrieve JSON content .+: No such file or directory/).and_call_original
92+
93+
expect(Puppet::Util::Json.load_file_if_valid('does/not/exist.json')).to eql(nil)
94+
end
95+
end
96+
97+
context '#load_file' do
98+
it_should_behave_like 'json file loader', Puppet::Util::Json.method(:load_file)
99+
100+
it 'raises an error when the file is invalid JSON' do
101+
file_path = file_containing('input', '{ invalid')
102+
103+
expect {
104+
Puppet::Util::Json.load_file(file_path)
105+
}.to raise_error(Puppet::Util::Json::ParseError, /unexpected token at '{ invalid'/)
106+
end
107+
108+
it 'raises an error when the filename is illegal' do
109+
expect {
110+
Puppet::Util::Json.load_file("not\0allowed")
111+
}.to raise_error(ArgumentError, /null byte/)
112+
end
113+
114+
it 'raises an error when the file does not exist' do
115+
expect {
116+
Puppet::Util::Json.load_file('does/not/exist.json')
117+
}.to raise_error(Errno::ENOENT, /No such file or directory/)
118+
end
119+
120+
it 'writes data formatted as JSON to disk' do
121+
file_path = file_containing('input', Puppet::Util::Json.dump({ "my" => "data" }))
122+
123+
expect(Puppet::Util::Json.load_file(file_path)).to eq({ "my" => "data" })
124+
end
125+
end
126+
end

0 commit comments

Comments
 (0)