Skip to content

Commit

Permalink
json resource (et. al.): allow inspec check to succeed when using com…
Browse files Browse the repository at this point in the history
…mand (#2317)

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

When using the `json` resource (or any of the resources that subclass
JsonConfig), `inspec check` would fail if the content was supplied with
the `command` option. This is because the `command` resource is mocked
and an empty string would be returned for `stdout`. That content would
be blindly passed to the `parse` method would which raise an exception
and cause `inspec check` to fail.

This change refactors JsonConfig to be a bit cleaner and use some helper
methods. Additionally, we use the new Exceptions to properly raise errors
which are naturally caught by Inspec::Profile, etc.

Signed-off-by: Adam Leff <adam@leff.co>

* Make `resource_base_name` method private

Signed-off-by: Adam Leff <adam@leff.co>
  • Loading branch information
adamleff committed Nov 27, 2017
1 parent 65046f9 commit 6c3ab70
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 51 deletions.
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

0 comments on commit 6c3ab70

Please sign in to comment.