Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

json resource (et. al.): allow inspec check to succeed when using command #2317

Merged
merged 2 commits into from Nov 27, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 8 additions & 2 deletions lib/resources/csv.rb
Expand Up @@ -34,6 +34,8 @@ def parse(content)

# convert to hash
csv.to_a.map(&:to_hash)
rescue => e
raise Inspec::Exceptions::ResourceFailed, "Unable to parse CSV: #{e.message}"
end

# override the value method from JsonConfig
Expand All @@ -45,8 +47,12 @@ def value(key)
@params.map { |x| x[key.first.to_s] }.compact
end

def to_s
"Csv #{@path}"
private

# used by JsonConfig to build up a full to_s method
# based on whether a file path, content, or command was supplied.
def resource_base_name
'CSV'
end
end
end
8 changes: 6 additions & 2 deletions lib/resources/ini.rb
Expand Up @@ -18,8 +18,12 @@ def parse(content)
SimpleConfig.new(content).params
end

def to_s
"INI #{@path}"
private

# used by JsonConfig to build up a full to_s method
# based on whether a file path, content, or command was supplied.
def resource_base_name
'INI'
end
end
end
99 changes: 59 additions & 40 deletions lib/resources/json.rb
Expand Up @@ -26,45 +26,11 @@ class JsonConfig < Inspec.resource(1)
include ObjectTraverser

# make params readable
attr_reader :params
attr_reader :params, :raw_content

def initialize(opts)
@opts = opts
if opts.is_a?(Hash)
if opts.key?(:content)
@file_content = opts[:content]
elsif opts.key?(:command)
@command = inspec.command(opts[:command])
@file_content = @command.stdout
end
else
@path = opts
@file = inspec.file(@opts)
@file_content = @file.content

# check if file is available
if !@file.file?
skip_resource "Can't find file \"#{@path}\""
return @params = {}
end

# check if file is readable
if @file_content.nil? && !@file.empty?
skip_resource "Can't read file \"#{@path}\""
return @params = {}
end
end

@params = parse(@file_content)
end

def parse(content)
require 'json'
JSON.parse(content)
end

def value(key)
extract_value(key, @params)
@raw_content = load_raw_content(opts)
@params = parse(@raw_content)
end

# Shorthand to retrieve a parameter name via `#its`.
Expand All @@ -79,12 +45,65 @@ def method_missing(*keys)
value(keys)
end

def value(key)
# uses ObjectTraverser.extract_value to walk the hash looking for the key,
# which may be an Array of keys for a nested Hash.
extract_value(key, params)
end

def to_s
if @opts.is_a?(Hash) && @opts.key?(:content)
'Json content'
"#{resource_base_name} #{@resource_name_supplement || 'content'}"
end

private

def parse(content)
require 'json'
JSON.parse(content)
rescue => e
raise Inspec::Exceptions::ResourceFailed, "Unable to parse JSON: #{e.message}"
end

def load_raw_content(opts)
# if the opts isn't a hash, we assume it's a path to a file
unless opts.is_a?(Hash)
@resource_name_supplement = opts
return load_raw_from_file(opts)
end

if opts.key?(:command)
@resource_name_supplement = "from command: #{opts[:command]}"
load_raw_from_command(opts[:command])
elsif opts.key?(:content)
opts[:content]
else
"Json #{@path}"
raise Inspec::Exceptions::ResourceFailed, 'No JSON content; must specify a file, command, or raw JSON content'
end
end

def load_raw_from_file(path)
file = inspec.file(path)

# these are currently ResourceSkipped to maintain consistency with the resource
# pre-refactor (which used skip_resource). These should likely be changed to
# ResourceFailed during a major version bump.
raise Inspec::Exceptions::ResourceSkipped, "No such file: #{path}" unless file.file?
raise Inspec::Exceptions::ResourceSkipped, "File #{path} is empty or is not readable by current user" if file.content.nil? || file.content.empty?

file.content
end

def load_raw_from_command(command)
command_output = inspec.command(command).stdout
raise Inspec::Exceptions::ResourceSkipped, "No output from command: #{command}" if command_output.nil? || command_output.empty?

command_output
end

# for resources the subclass JsonConfig, this allows specification of the resource
# base name in each subclass so we can build a good to_s method
def resource_base_name
'JSON'
end
end
end
10 changes: 8 additions & 2 deletions lib/resources/toml.rb
Expand Up @@ -17,10 +17,16 @@ class TomlConfig < JsonConfig

def parse(content)
Tomlrb.parse(content)
rescue => e
raise Inspec::Exceptions::ResourceFailed, "Unable to parse TOML: #{e.message}"
end

def to_s
"TOML #{@path}"
private

# used by JsonConfig to build up a full to_s method
# based on whether a file path, content, or command was supplied.
def resource_base_name
'TOML'
end
end
end
10 changes: 8 additions & 2 deletions lib/resources/xml.rb
Expand Up @@ -14,14 +14,20 @@ class XmlConfig < JsonConfig
def parse(content)
require 'rexml/document'
REXML::Document.new(content)
rescue => e
raise Inspec::Exceptions::ResourceFailed, "Unable to parse XML: #{e.message}"
end

def value(key)
REXML::XPath.each(@params, key.first.to_s).map(&:text)
end

def to_s
"XML #{@path}"
private

# used by JsonConfig to build up a full to_s method
# based on whether a file path, content, or command was supplied.
def resource_base_name
'XML'
end
end
end
10 changes: 8 additions & 2 deletions lib/resources/yaml.rb
Expand Up @@ -30,10 +30,16 @@ class YamlConfig < JsonConfig
# override file load and parse hash from yaml
def parse(content)
YAML.load(content)
rescue => e
raise Inspec::Exceptions::ResourceFailed, "Unable to parse YAML: #{e.message}"
end

def to_s
"YAML #{@path}"
private

# used by JsonConfig to build up a full to_s method
# based on whether a file path, content, or command was supplied.
def resource_base_name
'YAML'
end
end
end
64 changes: 63 additions & 1 deletion test/unit/resources/json_test.rb
Expand Up @@ -37,7 +37,69 @@
let (:resource) { load_resource('json', 'nonexistent.json') }

it 'produces an error' do
_(resource.resource_exception_message).must_equal 'Can\'t find file "nonexistent.json"'
_(resource.resource_exception_message).must_equal 'No such file: nonexistent.json'
end
end

describe '#load_raw_from_file' do
let(:path) { '/path/to/file.txt' }
let(:resource) { Inspec::Resources::JsonConfig.allocate }
let(:inspec) { mock }
let(:file) { mock }

before do
resource.stubs(:inspec).returns(inspec)
inspec.expects(:file).with(path).returns(file)
end

it 'raises an exception when the file does not exist' do
file.expects(:file?).returns(false)
proc { resource.send(:load_raw_from_file, path) }.must_raise Inspec::Exceptions::ResourceSkipped
end

it 'raises an exception if the file content is nil' do
file.expects(:file?).returns(true)
file.expects(:content).returns(nil)
proc { resource.send(:load_raw_from_file, path) }.must_raise Inspec::Exceptions::ResourceSkipped
end

it 'raises an exception if the file content is empty' do
file.expects(:file?).returns(true)
file.expects(:content).at_least_once.returns('')
proc { resource.send(:load_raw_from_file, path) }.must_raise Inspec::Exceptions::ResourceSkipped
end

it 'returns the file content' do
file.expects(:file?).returns(true)
file.expects(:content).at_least_once.returns('json goes here')
resource.send(:load_raw_from_file, path).must_equal 'json goes here'
end
end

describe '#load_raw_from_file' do
let(:cmd_str) { 'curl localhost' }
let(:resource) { Inspec::Resources::JsonConfig.allocate }
let(:inspec) { mock }
let(:command) { mock }

before do
resource.stubs(:inspec).returns(inspec)
inspec.expects(:command).with(cmd_str).returns(command)
end

it 'raises an exception if command stdout is nil' do
command.expects(:stdout).returns(nil)
proc { resource.send(:load_raw_from_command, cmd_str) }.must_raise Inspec::Exceptions::ResourceSkipped
end

it 'raises an exception if command stdout is empty' do
command.expects(:stdout).returns('')
proc { resource.send(:load_raw_from_command, cmd_str) }.must_raise Inspec::Exceptions::ResourceSkipped
end

it 'returns the command output' do
command.expects(:stdout).returns('json goes here')
resource.send(:load_raw_from_command, cmd_str).must_equal 'json goes here'
end
end
end