Internal overhaul of Inputs system (#3875)

Internal overhaul of Inputs system
This commit is contained in:
Clinton Wolfe 2019-04-26 13:59:51 -04:00 committed by GitHub
commit 535d7ae6a9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 1295 additions and 816 deletions

View file

@ -26,8 +26,23 @@ module Inspec
with_resource_dsl resources_dsl
# allow attributes to be accessed within control blocks
define_method :attribute do |name|
Inspec::InputRegistry.find_input(name, profile_id).value
# TODO: deprecate name, use input()
define_method :attribute do |input_name, options = {}|
if options.empty?
# Simply an access, no event here
Inspec::InputRegistry.find_or_register_input(input_name, profile_id).value
else
options[:priority] = 20
options[:provider] = :inline_control_code
evt = Inspec::Input.infer_event(options)
Inspec::InputRegistry.find_or_register_input(input_name, profile_name, event: evt).value
end
end
# Find the Input object, but don't collapse to a value.
# Will return nil on a miss.
define_method :input_object do |input_name|
Inspec::InputRegistry.find_or_register_input(input_name, profile_id)
end
# Support for Control DSL plugins.
@ -168,14 +183,25 @@ module Inspec
end
# method for inputs; import input handling
define_method :attribute do |name, options = nil|
if options.nil?
Inspec::InputRegistry.find_input(name, profile_id).value
# TODO: deprecate name, use input()
define_method :attribute do |input_name, options = {}|
if options.empty?
# Simply an access, no event here
Inspec::InputRegistry.find_or_register_input(input_name, profile_id).value
else
profile_context_owner.register_input(name, options)
options[:priority] = 20
options[:provider] = :inline_control_code
evt = Inspec::Input.infer_event(options)
Inspec::InputRegistry.find_or_register_input(input_name, profile_name, event: evt).value
end
end
# Find the Input object, but don't collapse to a value.
# Will return nil on a miss.
define_method :input_object do |input_name|
Inspec::InputRegistry.find_or_register_input(input_name, profile_id)
end
define_method :skip_control do |id|
profile_context_owner.unregister_rule(id)
end

View file

@ -118,6 +118,7 @@ module Inspec
return @profile unless @profile.nil?
opts = @opts.dup
opts[:backend] = @backend
opts[:runner_conf] = Inspec::Config.cached
if !@dependencies.nil? && !@dependencies.empty?
opts[:dependencies] = Inspec::DependencySet.from_array(@dependencies, @cwd, @cache, @backend)
end

View file

@ -23,6 +23,7 @@ module Inspec
# implementation of the fetcher being used.
#
class Resolver
# Here deps is an Array of Hashes
def self.resolve(dependencies, cache, working_dir, backend)
reqs = dependencies.map do |dep|
req = Inspec::Requirement.from_metadata(dep, cache, cwd: working_dir, backend: backend)
@ -47,6 +48,7 @@ module Inspec
end
end
# Here deps is an Array of Inspec::Requirement
def resolve(deps, top_level = true, seen_items = {}, path_string = '') # rubocop:disable Metrics/AbcSize
graph = {}
if top_level

View file

@ -79,7 +79,7 @@ module Inspec::DSL
def self.filter_included_controls(context, profile, &block)
mock = Inspec::Backend.create(Inspec::Config.mock)
include_ctx = Inspec::ProfileContext.for_profile(profile, mock, {})
include_ctx = Inspec::ProfileContext.for_profile(profile, mock)
include_ctx.load(block) if block_given?
# remove all rules that were not registered
context.all_rules.each do |r|

View file

@ -1,83 +1,224 @@
require 'forwardable'
require 'singleton'
require 'inspec/objects/input'
require 'inspec/secrets'
require 'inspec/exceptions'
module Inspec
# The InputRegistry's responsibilities include:
# - maintaining a list of Input objects that are bound to profiles
# - assisting in the lookup and creation of Inputs
class InputRegistry
include Singleton
extend Forwardable
attr_reader :list
def_delegator :list, :each
def_delegator :list, :[]
def_delegator :list, :key?, :profile_exist?
def_delegator :list, :select
# These self methods are convenience methods so you dont always
# have to specify instance when calling the registry
def self.find_input(name, profile)
instance.find_input(name, profile)
end
def self.register_input(name, profile, options = {})
instance.register_input(name, profile, options)
end
def self.register_profile_alias(name, alias_name)
instance.register_profile_alias(name, alias_name)
end
def self.list_inputs_for_profile(profile)
instance.list_inputs_for_profile(profile)
end
attr_reader :inputs_by_profile, :profile_aliases
def_delegator :inputs_by_profile, :each
def_delegator :inputs_by_profile, :[]
def_delegator :inputs_by_profile, :key?, :profile_known?
def_delegator :inputs_by_profile, :select
def_delegator :profile_aliases, :key?, :profile_alias?
def initialize
# this is a collection of profiles which have a value of input objects
@list = {}
# Keyed on String profile_name => Hash of String input_name => Input object
@inputs_by_profile = {}
# this is a list of optional profile name overrides set in the inspec.yml
@profile_aliases = {}
end
def find_input(name, profile)
profile = @profile_aliases[profile] if !profile_exist?(profile) && @profile_aliases[profile]
unless profile_exist?(profile)
error = Inspec::InputRegistry::ProfileLookupError.new
error.profile_name = profile
raise error, "Profile '#{error.profile_name}' does not have any inputs"
end
unless list[profile].key?(name)
error = Inspec::InputRegistry::InputLookupError.new
error.input_name = name
error.profile_name = profile
raise error, "Profile '#{error.profile_name}' does not have an input with name '#{error.input_name}'"
end
list[profile][name]
end
def register_input(name, profile, options = {})
# check for a profile override name
if profile_exist?(profile) && list[profile][name] && options.empty?
list[profile][name]
else
list[profile] = {} unless profile_exist?(profile)
list[profile][name] = Inspec::Input.new(name, options)
end
end
#-------------------------------------------------------------#
# Support for Profiles
#-------------------------------------------------------------#
def register_profile_alias(name, alias_name)
@profile_aliases[name] = alias_name
end
def list_inputs_for_profile(profile)
list[profile] = {} unless profile_exist?(profile)
list[profile]
inputs_by_profile[profile] = {} unless profile_known?(profile)
inputs_by_profile[profile]
end
#-------------------------------------------------------------#
# Support for Individual Inputs
#-------------------------------------------------------------#
def find_or_register_input(input_name, profile_name, options = {})
if profile_alias?(profile_name)
alias_name = profile_name
profile_name = profile_aliases[profile_name]
handle_late_arriving_alias(alias_name, profile_name) if profile_known?(alias_name)
end
inputs_by_profile[profile_name] ||= {}
if inputs_by_profile[profile_name].key?(input_name)
inputs_by_profile[profile_name][input_name].update(options)
else
inputs_by_profile[profile_name][input_name] = Inspec::Input.new(input_name, options)
end
inputs_by_profile[profile_name][input_name]
end
# It is possible for a wrapper profile to create an input in metadata,
# referring to the child profile by an alias that has not yet been registered.
# The registry will then store the inputs under the alias, as if the alias
# were a true profile.
# If that happens and the child profile also mentions the input, we will
# need to move some things - all inputs should be stored under the true
# profile name, and no inputs should be stored under the alias.
def handle_late_arriving_alias(alias_name, profile_name)
inputs_by_profile[profile_name] ||= {}
inputs_by_profile[alias_name].each do |input_name, input_from_alias|
# Move the inpuut, or if it exists, merge events
existing = inputs_by_profile[profile_name][input_name]
if existing
existing.events.concat(input_from_alias.events)
else
inputs_by_profile[profile_name][input_name] = input_from_alias
end
end
# Finally, delete the (now copied-out) entry for the alias
inputs_by_profile.delete(alias_name)
end
#-------------------------------------------------------------#
# Support for Binding Inputs
#-------------------------------------------------------------#
# This method is called by the Profile as soon as it has
# enough context to allow binding inputs to it.
def bind_profile_inputs(profile_name, sources = {})
inputs_by_profile[profile_name] ||= {}
# In a more perfect world, we could let the core plugins choose
# self-determine what to do; but as-is, the APIs that call this
# are a bit over-constrained.
bind_inputs_from_metadata(profile_name, sources[:profile_metadata])
bind_inputs_from_input_files(profile_name, sources[:cli_input_files])
bind_inputs_from_runner_api(profile_name, sources[:runner_api])
end
private
def bind_inputs_from_runner_api(profile_name, input_hash)
# TODO: move this into a core plugin
return if input_hash.nil?
return if input_hash.empty?
# These arrive as a bare hash - values are raw values, not options
input_hash.each do |input_name, input_value|
loc = Inspec::Input::Event.probe_stack # TODO: likely modify this to look for a kitchen.yml, if that is realistic
evt = Inspec::Input::Event.new(
value: input_value,
provider: :runner_api, # TODO: suss out if audit cookbook or kitchen-inspec or something unknown
priority: 40,
file: loc.path,
line: loc.lineno,
)
find_or_register_input(input_name, profile_name, event: evt)
end
end
def bind_inputs_from_input_files(profile_name, file_list)
# TODO: move this into a core plugin
return if file_list.nil?
return if file_list.empty?
file_list.each do |path|
validate_inputs_file_readability!(path)
# TODO: drop this SecretsBackend stuff, will be handled by plugin system
data = Inspec::SecretsBackend.resolve(path)
if data.nil?
raise Inspec::Exceptions::SecretsBackendNotFound,
"Cannot find parser for inputs file '#{path}'. " \
'Check to make sure file has the appropriate extension.'
end
next if data.inputs.nil?
data.inputs.each do |input_name, input_value|
evt = Inspec::Input::Event.new(
value: input_value,
provider: :cli_files,
priority: 40,
file: path,
# TODO: any way we could get a line number?
)
find_or_register_input(input_name, profile_name, event: evt)
end
end
end
def validate_inputs_file_readability!(path)
unless File.exist?(path)
raise Inspec::Exceptions::InputsFileDoesNotExist,
"Cannot find input file '#{path}'. " \
'Check to make sure file exists.'
end
unless File.readable?(path)
raise Inspec::Exceptions::InputsFileNotReadable,
"Cannot read input file '#{path}'. " \
'Check to make sure file is readable.'
end
true
end
def bind_inputs_from_metadata(profile_name, profile_metadata_obj)
# TODO: move this into a core plugin
# TODO: add deprecation stuff
return if profile_metadata_obj.nil? # Metadata files are technically optional
if profile_metadata_obj.params.key?(:attributes) && profile_metadata_obj.params[:attributes].is_a?(Array)
profile_metadata_obj.params[:attributes].each do |input_orig|
input_options = input_orig.dup
input_name = input_options.delete(:name)
input_options.merge!({ priority: 30, provider: :profile_metadata, file: File.join(profile_name, 'inspec.yml') })
evt = Inspec::Input.infer_event(input_options)
# Profile metadata may set inputs in other profiles by naming them.
if input_options[:profile]
profile_name = input_options[:profile] || profile_name
# Override priority to force this to win. Allow user to set their own priority.
evt.priority = input_orig[:priority] || 35
end
find_or_register_input(input_name,
profile_name,
type: input_options[:type],
required: input_options[:required],
event: evt)
end
elsif profile_metadata_obj.params.key?(:attributes)
Inspec::Log.warn 'Inputs must be defined as an Array. Skipping current definition.'
end
end
#-------------------------------------------------------------#
# Other Support
#-------------------------------------------------------------#
public
# Used in testing
def __reset
@list = {}
@inputs_by_profile = {}
@profile_aliases = {}
end
# These class methods are convenience methods so you don't always
# have to call #instance when calling the registry
[
:find_or_register_input,
:register_profile_alias,
:list_inputs_for_profile,
:bind_profile_inputs,
].each do |meth|
define_singleton_method(meth) do |*args|
instance.send(meth, *args)
end
end
end
end

View file

@ -14,6 +14,73 @@ end
module Inspec
class Input
#===========================================================================#
# Class Input::Event
#===========================================================================#
# Information about how the input obtained its value.
# Each time it changes, an Input::Event is added to the #events array.
class Event
EVENT_PROPERTIES = [
:action, # :create, :set, :fetch
:provider, # Name of the plugin
:priority, # Priority of this plugin for resolving conflicts. 1-100, higher numbers win.
:value, # New value, if provided.
:file, # File containing the input-changing action, if known
:line, # Line in file containing the input-changing action, if known
:hit, # if action is :fetch, true if the remote source had the input
].freeze
# Value has a special handler
EVENT_PROPERTIES.reject { |p| p == :value }.each do |prop|
attr_accessor prop
end
attr_reader :value
def initialize(properties = {})
@value_has_been_set = false
properties.each do |prop_name, prop_value|
if EVENT_PROPERTIES.include? prop_name
# OK, save the property
send((prop_name.to_s + '=').to_sym, prop_value)
else
raise "Unrecognized property to Input::Event: #{prop_name}"
end
end
end
def value=(the_val)
# Even if set to nil or false, it has indeed been set; note that fact.
@value_has_been_set = true
@value = the_val
end
def value_has_been_set?
@value_has_been_set
end
def diagnostic_string
to_h.reject { |_, val| val.nil? }.to_a.map { |pair| "#{pair[0]}: '#{pair[1]}'" }.join(', ')
end
def to_h
EVENT_PROPERTIES.each_with_object({}) do |prop, hash|
hash[prop] = send(prop)
end
end
def self.probe_stack
frames = caller_locations(2, 40)
frames.reject! { |f| f.path && f.path.include?('/lib/inspec/') }
frames.first
end
end
#===========================================================================#
# Class NO_VALUE_SET
#===========================================================================#
# This special class is used to represent the value when an input has
# not been assigned a value. This allows a user to explicitly assign nil
# to an input.
@ -62,8 +129,11 @@ module Inspec
end
class Input
attr_accessor :name
#===========================================================================#
# Class Inspec::Input
#===========================================================================#
# Validation types for input values
VALID_TYPES = %w{
String
Numeric
@ -74,6 +144,21 @@ module Inspec
Any
}.freeze
# If you call `input` in a control file, the input will receive this priority.
# You can override that with a :priority option.
DEFAULT_PRIORITY_FOR_DSL_ATTRIBUTES = 20
# If you somehow manage to initialize an Input outside of the DSL,
# AND you don't provide an Input::Event, this is the priority you get.
DEFAULT_PRIORITY_FOR_UNKNOWN_CALLER = 10
# If you directly call value=, this is the priority assigned.
# This is the highest priority within InSpec core; though plugins
# are free to go higher.
DEFAULT_PRIORITY_FOR_VALUE_SET = 60
attr_reader :description, :events, :identifier, :name, :required, :title, :type
def initialize(name, options = {})
@name = name
@opts = options
@ -82,49 +167,164 @@ module Inspec
if @opts.key?(:value)
Inspec::Log.warn "Input #{@name} created using both :default and :value options - ignoring :default"
@opts.delete(:default)
else
@opts[:value] = @opts.delete(:default)
end
end
@value = @opts[:value]
validate_value_type(@value) if @opts.key?(:type) && @opts.key?(:value)
end
def value=(new_value)
validate_value_type(new_value) if @opts.key?(:type)
@value = new_value
# Array of Input::Event objects. These compete with one another to determine
# the value of the input when value() is called, as well as providing a
# debugging record of when and how the value changed.
@events = []
events.push make_creation_event(options)
update(options)
end
def set_events
events.select { |e| e.action == :set }
end
def diagnostic_string
"Input #{name}, with history:\n" +
events.map(&:diagnostic_string).map { |line| " #{line}" }.join("\n")
end
#--------------------------------------------------------------------------#
# Managing Value
#--------------------------------------------------------------------------#
def update(options)
_update_set_metadata(options)
normalize_type_restriction!
# Values are set by passing events in; but we can also infer an event.
if options.key?(:value) || options.key?(:default)
if options.key?(:event)
if options.key?(:value) || options.key?(:default)
Inspec::Log.warn "Do not provide both an Event and a value as an option to attribute('#{name}') - using value from event"
end
else
self.class.infer_event(options) # Sets options[:event]
end
end
events << options[:event] if options.key? :event
enforce_type_restriction!
end
# We can determine a value:
# 1. By event.value (preferred)
# 2. By options[:value]
# 3. By options[:default] (deprecated)
def self.infer_event(options)
# Don't rely on this working; you really should be passing a proper Input::Event
# with the context information you have.
location = Input::Event.probe_stack
event = Input::Event.new(
action: :set,
provider: options[:provider] || :unknown,
priority: options[:priority] || Inspec::Input::DEFAULT_PRIORITY_FOR_UNKNOWN_CALLER,
file: location.path,
line: location.lineno,
)
if options.key?(:default)
Inspec.deprecate(:attrs_value_replaces_default, "attribute name: '#{name}'")
if options.key?(:value)
Inspec::Log.warn "Input #{@name} created using both :default and :value options - ignoring :default"
options.delete(:default)
else
options[:value] = options.delete(:default)
end
end
event.value = options[:value] if options.key?(:value)
options[:event] = event
end
private
def _update_set_metadata(options)
# Basic metadata
@title = options[:title] if options.key?(:title)
@description = options[:description] if options.key?(:description)
@required = options[:required] if options.key?(:required)
@identifier = options[:identifier] if options.key?(:identifier) # TODO: determine if this is ever used
@type = options[:type] if options.key?(:type)
end
def make_creation_event(options)
loc = options[:location] || Event.probe_stack
Input::Event.new(
action: :create,
provider: options[:provider],
file: loc.path,
line: loc.lineno,
)
end
# Determine the current winning value, but don't validate it
def current_value
# Examine the events to determine highest-priority value. Tie-break
# by using the last one set.
events_that_set_a_value = events.select(&:value_has_been_set?)
winning_priority = events_that_set_a_value.map(&:priority).max
winning_events = events_that_set_a_value.select { |e| e.priority == winning_priority }
winning_event = winning_events.last # Last for tie-break
if winning_event.nil?
# No value has been set - return special no value object
NO_VALUE_SET.new(name)
else
winning_event.value # May still be nil
end
end
public
def value=(new_value, priority = DEFAULT_PRIORITY_FOR_VALUE_SET)
# Inject a new Event with the new value.
location = Event.probe_stack
events << Event.new(
action: :set,
provider: :value_setter,
priority: priority,
value: new_value,
file: location.path,
line: location.lineno,
)
enforce_type_restriction!
new_value
end
def value
if @value.nil?
validate_required(@value) if @opts[:required] == true
@value = value_or_dummy
else
@value
end
enforce_required_validation!
current_value
end
def title
@opts[:title]
def has_value?
!current_value.is_a? NO_VALUE_SET
end
def description
@opts[:description]
#--------------------------------------------------------------------------#
# Marshalling
#--------------------------------------------------------------------------#
def to_hash
as_hash = { name: name, options: {} }
[:description, :title, :identifier, :type, :required, :value].each do |field|
val = send(field)
next if val.nil?
as_hash[:options][field] = val
end
as_hash
end
def ruby_var_identifier
@opts[:identifier] || 'attr_' + @name.downcase.strip.gsub(/\s+/, '-').gsub(/[^\w-]/, '')
end
def to_hash
{
name: @name,
options: @opts,
}
identifier || 'attr_' + name.downcase.strip.gsub(/\s+/, '-').gsub(/[^\w-]/, '')
end
def to_ruby
res = ["#{ruby_var_identifier} = attribute('#{@name}',{"]
res = ["#{ruby_var_identifier} = attribute('#{name}',{"]
res.push " title: '#{title}'," unless title.to_s.empty?
res.push " value: #{value.inspect}," unless value.to_s.empty?
# to_ruby may generate code that is to be used by older versions of inspec.
@ -136,37 +336,78 @@ module Inspec
res.join("\n")
end
#--------------------------------------------------------------------------#
# Value Type Coercion
#--------------------------------------------------------------------------#
def to_s
"Input #{@name} with #{@value}"
"Input #{name} with #{current_value}"
end
#--------------------------------------------------------------------------#
# Validation
#--------------------------------------------------------------------------#
private
def validate_required(value)
def enforce_required_validation!
return unless required
# skip if we are not doing an exec call (archive/vendor/check)
return unless Inspec::BaseCLI.inspec_cli_command == :exec
# value will be set already if a secrets file was passed in
if (!@opts.key?(:default) && value.nil?) || (@opts[:default].nil? && value.nil?)
proposed_value = current_value
if proposed_value.nil? || proposed_value.is_a?(NO_VALUE_SET)
error = Inspec::Input::RequiredError.new
error.input_name = @name
error.input_name = name
raise error, "Input '#{error.input_name}' is required and does not have a value."
end
end
def validate_type(type)
type = type.capitalize
def enforce_type_restriction! # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
return unless type
return unless has_value?
type_req = type
return if type_req == 'Any'
proposed_value = current_value
invalid_type = false
if type_req == 'Regexp'
invalid_type = true if !valid_regexp?(proposed_value)
elsif type_req == 'Numeric'
invalid_type = true if !valid_numeric?(proposed_value)
elsif type_req == 'Boolean'
invalid_type = true if ![true, false].include?(proposed_value)
elsif proposed_value.is_a?(Module.const_get(type_req)) == false
# TODO: why is this case here?
invalid_type = true
end
if invalid_type == true
error = Inspec::Input::ValidationError.new
error.input_name = @name
error.input_value = proposed_value
error.input_type = type_req
raise error, "Input '#{error.input_name}' with value '#{error.input_value}' does not validate to type '#{error.input_type}'."
end
end
def normalize_type_restriction!
return unless type
type_req = type.capitalize
abbreviations = {
'Num' => 'Numeric',
'Regex' => 'Regexp',
}
type = abbreviations[type] if abbreviations.key?(type)
if !VALID_TYPES.include?(type)
type_req = abbreviations[type_req] if abbreviations.key?(type_req)
if !VALID_TYPES.include?(type_req)
error = Inspec::Input::TypeError.new
error.input_type = type
error.input_type = type_req
raise error, "Type '#{error.input_type}' is not a valid input type."
end
type
@type = type_req
end
def valid_numeric?(value)
@ -177,41 +418,11 @@ module Inspec
end
def valid_regexp?(value)
# check for invalid regex syntex
# check for invalid regex syntax
Regexp.new(value)
true
rescue
false
end
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def validate_value_type(value)
type = validate_type(@opts[:type])
return if type == 'Any'
invalid_type = false
if type == 'Regexp'
invalid_type = true if !value.is_a?(String) || !valid_regexp?(value)
elsif type == 'Numeric'
invalid_type = true if !valid_numeric?(value)
elsif type == 'Boolean'
invalid_type = true if ![true, false].include?(value)
elsif value.is_a?(Module.const_get(type)) == false
invalid_type = true
end
if invalid_type == true
error = Inspec::Input::ValidationError.new
error.input_name = @name
error.input_value = value
error.input_type = type
raise error, "Input '#{error.input_name}' with value '#{error.input_value}' does not validate to type '#{error.input_type}'."
end
end
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def value_or_dummy
@opts.key?(:value) ? @opts[:value] : Inspec::Input::NO_VALUE_SET.new(@name)
end
end
end

View file

@ -81,7 +81,7 @@ module Inspec
end
attr_reader :source_reader, :backend, :runner_context, :check_mode
attr_accessor :parent_profile, :profile_name
attr_accessor :parent_profile, :profile_id, :profile_name
def_delegator :@source_reader, :tests
def_delegator :@source_reader, :libraries
def_delegator :@source_reader, :metadata
@ -118,25 +118,32 @@ module Inspec
@runtime_profile = RuntimeProfile.new(self)
@backend.profile = @runtime_profile
# The AttributeRegistry is in charge of keeping track of inputs;
# it is the single source of truth. Now that we have a profile object,
# we can create any inputs that were provided by various mechanisms.
options[:runner_conf] ||= Inspec::Config.cached
if options[:runner_conf].key?(:attrs)
Inspec.deprecate(:rename_attributes_to_inputs, 'Use --input-file on the command line instead of --attrs.')
options[:runner_conf][:input_file] = options[:runner_conf].delete(:attrs)
end
Inspec::InputRegistry.bind_profile_inputs(
# Every input only exists in the context of a profile
metadata.params[:name], # TODO: test this with profile aliasing
# Remaining args are possible sources of inputs
cli_input_files: options[:runner_conf][:input_file], # From CLI --input-file
profile_metadata: metadata,
# TODO: deprecation checks here
runner_api: options[:runner_conf][:attributes], # This is the route the audit_cookbook and kitchen-inspec take
)
@runner_context =
options[:profile_context] ||
Inspec::ProfileContext.for_profile(self, @backend, @input_values)
Inspec::ProfileContext.for_profile(self, @backend)
@supports_platform = metadata.supports_platform?(@backend)
@supports_runtime = metadata.supports_runtime?
register_metadata_inputs
end
def register_metadata_inputs # TODO: deprecate
if metadata.params.key?(:attributes) && metadata.params[:attributes].is_a?(Array)
metadata.params[:attributes].each do |attribute|
attr_dup = attribute.dup
name = attr_dup.delete(:name)
@runner_context.register_input(name, attr_dup)
end
elsif metadata.params.key?(:attributes)
Inspec::Log.warn 'Inputs must be defined as an Array. Skipping current definition.'
end
end
def name
@ -595,7 +602,7 @@ module Inspec
f = load_rule_filepath(prefix, rule)
load_rule(rule, f, controls, groups)
end
params[:inputs] = @runner_context.inputs
params[:inputs] = Inspec::InputRegistry.list_inputs_for_profile(@profile_id)
params
end

View file

@ -12,13 +12,11 @@ require 'inspec/objects/input'
module Inspec
class ProfileContext
def self.for_profile(profile, backend, inputs)
new(profile.name, backend, { 'profile' => profile,
'inputs' => inputs,
'check_mode' => profile.check_mode })
def self.for_profile(profile, backend)
new(profile.name, backend, { 'profile' => profile, 'check_mode' => profile.check_mode })
end
attr_reader :inputs, :backend, :profile_name, :profile_id, :resource_registry
attr_reader :backend, :profile_name, :profile_id, :resource_registry
attr_accessor :rules
def initialize(profile_id, backend, conf)
if backend.nil?
@ -35,7 +33,8 @@ module Inspec
@lib_subcontexts = []
@require_loader = ::Inspec::RequireLoader.new
Inspec::InputRegistry.register_profile_alias(@profile_id, @profile_name) if @profile_id != @profile_name
@inputs = Inspec::InputRegistry.list_inputs_for_profile(@profile_id)
# TODO: consider polling input source plugins; this is a bulk fetch opportunity
# A local resource registry that only contains resources defined
# in the transitive dependency tree of the loaded profile.
@resource_registry = Inspec::Resource.new_registry
@ -43,6 +42,10 @@ module Inspec
@current_load = nil
end
def attributes
Inspec::AttributeRegistry.list_attributes_for_profile(@profile_id)
end
def dependencies
if @conf['profile'].nil?
{}
@ -187,13 +190,6 @@ module Inspec
end
end
def register_input(name, options = {})
# we need to return an input object, to allow dermination of values
input = Inspec::InputRegistry.register_input(name, @profile_id, options)
input.value = @conf['inputs'][name] unless @conf['inputs'].nil? || @conf['inputs'][name].nil?
input.value
end
def set_header(field, val)
@current_load[field] = val
end

View file

@ -66,9 +66,13 @@ end
class RSpec::Core::ExampleGroup
# This DSL method allows us to access the values of inputs within InSpec tests
def attribute(name)
Inspec::InputRegistry.find_input(name, self.class.metadata[:profile_id]).value
Inspec::InputRegistry.find_or_register_input(name, self.class.metadata[:profile_id]).value
end
define_example_method :attribute
def input_obj(name)
Inspec::InputRegistry.find_or_register_input(name, self.class.metadata[:profile_id])
end
define_example_method :input_obj
# Here, we have to ensure our method_missing gets called prior
# to RSpec::Core::ExampleGroup.method_missing (the class method).

View file

@ -9,7 +9,6 @@ require 'inspec/backend'
require 'inspec/profile_context'
require 'inspec/profile'
require 'inspec/metadata'
require 'inspec/secrets'
require 'inspec/config'
require 'inspec/dependencies/cache'
# spec requirements
@ -32,7 +31,7 @@ module Inspec
class Runner
extend Forwardable
attr_reader :backend, :rules, :inputs
attr_reader :backend, :rules
def attributes
Inspec.deprecate(:rename_attributes_to_inputs, "Don't call runner.attributes, call runner.inputs")
@ -57,10 +56,17 @@ module Inspec
RunnerRspec.new(@conf)
end
# list of profile inputs
@inputs = {}
# About reading inputs:
# @conf gets passed around a lot, eventually to
# Inspec::InputRegistry.register_external_inputs.
#
# @conf may contain the key :attributes or :inputs, which is to be a Hash
# of values passed in from the Runner API.
# This is how kitchen-inspec and the audit_cookbook pass in inputs.
#
# @conf may contain the key :attrs or :input_file, which is to be an Array
# of file paths, each a YAML file. This how --input-file works.
load_inputs(@conf)
configure_transport
end
@ -101,7 +107,6 @@ module Inspec
@test_collector.add_profile(requirement.profile)
end
@inputs = profile.runner_context.inputs if @inputs.empty?
tests = profile.collect_tests
all_controls += tests unless tests.nil?
end
@ -149,35 +154,6 @@ module Inspec
@test_collector.exit_code
end
# determine all inputs before the execution, fetch data from secrets backend
def load_inputs(options)
# TODO: - rename :attributes - it is user-visible
options[:attributes] ||= {}
if options.key?(:attrs)
Inspec.deprecate(:rename_attributes_to_inputs, 'Use --input-file on the command line instead of --attrs.')
options[:input_file] = options.delete(:attrs)
end
secrets_targets = options[:input_file]
return options[:attributes] if secrets_targets.nil?
secrets_targets.each do |target|
validate_inputs_file_readability!(target)
secrets = Inspec::SecretsBackend.resolve(target)
if secrets.nil?
raise Inspec::Exceptions::SecretsBackendNotFound,
"Cannot find parser for inputs file '#{target}'. " \
'Check to make sure file has the appropriate extension.'
end
next if secrets.inputs.nil?
options[:attributes].merge!(secrets.inputs)
end
options[:attributes]
end
#
# add_target allows the user to add a target whose tests will be
# run when the user calls the run method.
@ -209,7 +185,7 @@ module Inspec
vendor_cache: @cache,
backend: @backend,
controls: @controls,
inputs: @conf[:attributes]) # TODO: read form :inputs here (user visible)
runner_conf: @conf)
raise "Could not resolve #{target} to valid input." if profile.nil?
@target_profiles << profile if supports_profile?(profile)
end
@ -300,22 +276,6 @@ module Inspec
examples.each { |e| @test_collector.add_test(e, rule) }
end
def validate_inputs_file_readability!(target)
unless File.exist?(target)
raise Inspec::Exceptions::InputsFileDoesNotExist,
"Cannot find input file '#{target}'. " \
'Check to make sure file exists.'
end
unless File.readable?(target)
raise Inspec::Exceptions::InputsFileNotReadable,
"Cannot read input file '#{target}'. " \
'Check to make sure file is readable.'
end
true
end
def rspec_skipped_block(arg, opts, message)
@test_collector.example_group(*arg, opts) do
# Send custom `it` block to RSpec

View file

@ -52,6 +52,27 @@ module Inspec
suffix = stdout.end_with?("\n") ? "\n" : ''
stdout.split("\n").reject { |l| l.include? ' DEPRECATION: ' }.join("\n") + suffix
end
# This works if you use json: true on an exec call
def must_have_all_controls_passing
# Strategy: assemble an array of tests that failed or skipped, and insist it is empty
# result.payload.json['profiles'][0]['controls'][0]['results'][0]['status']
failed_tests = []
payload.json['profiles'].each do |profile_struct|
profile_name = profile_struct['name']
profile_struct['controls'].each do |control_struct|
control_name = control_struct['id']
control_struct['results'].compact.each do |test_struct|
test_desc = test_struct['code_desc']
if test_struct['status'] != 'passed'
failed_tests << "#{profile_name}/#{control_name}/#{test_desc}"
end
end
end
end
failed_tests.must_be_empty
end
end
end

View file

@ -5,6 +5,9 @@ require 'functional/helper'
describe 'inputs' do
include FunctionalHelper
let(:inputs_profiles_path) { File.join(profile_path, 'inputs') }
# This tests being able to load complex structures from
# cli option-specified files.
[
'flat',
'nested',
@ -15,9 +18,9 @@ describe 'inputs' do
cmd += ' --no-create-lockfile'
cmd += ' --input-file ' + File.join(inputs_profiles_path, 'basic', 'files', "#{input_file}.yaml")
cmd += ' --controls ' + input_file
out = inspec(cmd)
out.stderr.must_equal ''
out.exit_status.must_equal 0
result = run_inspec_process(cmd)
result.stderr.must_equal ''
result.exit_status.must_equal 0
end
end
@ -58,52 +61,49 @@ describe 'inputs' do
end
end
describe 'run profile with yaml inputs' do
it "runs using yml inputs" do
describe 'when accessing inputs in a variety of scopes' do
it "is able to read the inputs" do
cmd = 'exec '
cmd += File.join(inputs_profiles_path, 'global')
cmd += ' --no-create-lockfile'
cmd += ' --input-file ' + File.join(inputs_profiles_path, 'global', 'files', "inputs.yml")
out = inspec(cmd)
out.stderr.must_equal ''
# TODO: fix attribute inheritance override test
# we have one failing case on this - run manually to see
# For now, reduce cases to 20; we'll be reworking all this soon anyway
# out.stdout.must_include '21 successful'
# out.exit_status.must_equal 0
out.stdout.must_include '20 successful' # and one failing
cmd += File.join(inputs_profiles_path, 'scoping')
result = run_inspec_process(cmd, json: true)
result.must_have_all_controls_passing
end
end
describe 'run profile with metadata inputs' do
it "does not error when inputs are empty" do
cmd = 'exec '
cmd += File.join(inputs_profiles_path, 'metadata-empty')
cmd += ' --no-create-lockfile'
out = inspec(cmd)
out.stdout.must_include 'WARN: Inputs must be defined as an Array. Skipping current definition.'
out.exit_status.must_equal 0
result = run_inspec_process(cmd, json: true)
result.stderr.must_include 'WARN: Inputs must be defined as an Array. Skipping current definition.'
result.exit_status.must_equal 0
end
it "errors with invalid input types" do
cmd = 'exec '
cmd += File.join(inputs_profiles_path, 'metadata-invalid')
cmd += ' --no-create-lockfile'
out = inspec(cmd)
out.stderr.must_equal "Type 'Color' is not a valid input type.\n"
out.stdout.must_equal ''
out.exit_status.must_equal 1
result = run_inspec_process(cmd, json: true)
result.stderr.must_equal "Type 'Color' is not a valid input type.\n"
result.exit_status.must_equal 1
end
it "errors with required input not defined" do
cmd = 'exec '
cmd += File.join(inputs_profiles_path, 'required')
cmd += ' --no-create-lockfile'
out = inspec(cmd)
out.stderr.must_equal "Input 'username' is required and does not have a value.\n"
out.stdout.must_equal ''
out.exit_status.must_equal 1
cmd += File.join(inputs_profiles_path, 'metadata-required')
result = run_inspec_process(cmd, json: true)
result.stderr.must_include "Input 'a_required_input' is required and does not have a value.\n"
result.exit_status.must_equal 1
end
# TODO - add test for backwards compatibility using 'attribute' in DSL
describe 'when profile inheritance is used' do
it 'should correctly assign input values using namespacing' do
cmd = 'exec ' + File.join(inputs_profiles_path, 'inheritance', 'wrapper')
result = run_inspec_process(cmd, json: true)
result.must_have_all_controls_passing
end
end
# # TODO - add test for backwards compatibility using 'attribute' in DSL
end
end

View file

@ -6,4 +6,6 @@ require 'minitest/pride'
require 'json'
require 'ostruct'
require 'byebug'
require_relative 'lib/resource_support/aws'

View file

@ -0,0 +1,131 @@
require 'helper'
require 'inspec/objects/input'
describe 'Inspec::Input and Events' do
let(:ipt) { Inspec::Input.new('input') }
#==============================================================#
# Create Event
#==============================================================#
describe 'when creating an input' do
it 'should have a creation event' do
creation_events = ipt.events.select { |e| e.action == :create }
creation_events.wont_be_empty
end
it 'should only have a creation event if no value was provided' do
creation_events = ipt.events.select { |e| e.action == :create }
creation_events.count.must_equal 1
end
it 'should have a create and a set event if a value was provided' do
ipt = Inspec::Input.new('input', value: 42)
creation_events = ipt.events.select { |e| e.action == :create }
creation_events.count.must_equal 1
set_events = ipt.set_events
set_events.count.must_equal 1
set_events.first.value.must_equal 42
end
end
#==============================================================#
# Set Events
#==============================================================#
describe 'when setting an input using value=' do
it 'should add a set event' do
ipt.set_events.count.must_equal 0
ipt.value = 42
ipt.set_events.count.must_equal 1
end
it 'should add one event for each value= operation' do
ipt.set_events.count.must_equal 0
ipt.value = 1
ipt.value = 2
ipt.value = 3
ipt.set_events.count.must_equal 3
end
end
#==============================================================#
# Picking a Winner
#==============================================================#
# For more real-world testing of metadata vs --attrs vs inline, see
# test/functional/inputs_test.rb
describe 'priority voting' do
it 'value() should return the correct value when there is just one set operation' do
evt = Inspec::Input::Event.new(value: 42, priority: 25, action: :set)
ipt.update(event: evt)
ipt.value.must_equal 42
end
it 'should return the highest priority regardless of order' do
evt1 = Inspec::Input::Event.new(value: 1, priority: 25, action: :set)
ipt.update(event: evt1)
evt2 = Inspec::Input::Event.new(value: 2, priority: 35, action: :set)
ipt.update(event: evt2)
evt3 = Inspec::Input::Event.new(value: 3, priority: 15, action: :set)
ipt.update(event: evt3)
ipt.value.must_equal 2
end
it 'breaks ties using the last event of the highest priority' do
evt1 = Inspec::Input::Event.new(value: 1, priority: 15, action: :set)
ipt.update(event: evt1)
evt2 = Inspec::Input::Event.new(value: 2, priority: 25, action: :set)
ipt.update(event: evt2)
evt3 = Inspec::Input::Event.new(value: 3, priority: 25, action: :set)
ipt.update(event: evt3)
ipt.value.must_equal 3
end
end
#==============================================================#
# Stack Hueristics
#==============================================================#
describe 'when determining where the call came from' do
it 'should get the line and file correct in the constructor' do
expected_file = __FILE__
expected_line = __LINE__; ipt = Inspec::Input.new('some_input') # Important to keep theses on one line
event = ipt.events.first
event.file.must_equal expected_file
event.line.must_equal expected_line
end
end
#==============================================================#
# Diagnostics
#==============================================================#
describe 'input diagnostics' do
it 'should dump the events' do
evt1 = Inspec::Input::Event.new(value: {a:1, b:2}, priority: 15, action: :set, provider: :unit_test)
ipt.update(event: evt1)
evt2 = Inspec::Input::Event.new(action: :fetch, provider: :alcubierre, hit: false)
ipt.update(event: evt2)
evt3 = Inspec::Input::Event.new(value: 12, action: :set, provider: :control_dsl, file: '/tmp/some/file.rb', line: 2)
ipt.update(event: evt3)
text = ipt.diagnostic_string
lines = text.split("\n")
lines.count.must_equal 5 # 3 events above + 1 create + 1 input name line
lines.shift # Not testing the inputs top line here
lines.each do |line|
line.must_match /^\s\s([a-z]+:\s\'.+\',\s)*?([a-z]+:\s\'.+\')$/ # key: 'value', key: 'value' ...
end
lines[0].must_include "action: 'create',"
lines[1].must_include "action: 'set',"
lines[1].must_include "value: '{" # It should to_s the value
lines[1].must_include "provider: 'unit_test'"
lines[1].must_include "priority: '15'"
end
end
end

View file

@ -2,6 +2,7 @@
require 'helper'
require 'inspec/input_registry'
require 'inspec/secrets'
describe Inspec::InputRegistry do
let(:registry) { Inspec::InputRegistry }
@ -12,60 +13,142 @@ describe Inspec::InputRegistry do
describe 'creating a profile input' do
it 'creates an input without options' do
registry.register_input('test_input', 'dummy_profile')
# confirm we get the dummy default
registry.find_input('test_input', 'dummy_profile').value.class.must_equal Inspec::Input::NO_VALUE_SET
registry.find_or_register_input('test_input', 'dummy_profile')
# confirm we get the dummy value
registry.find_or_register_input('test_input', 'dummy_profile').value.class.must_equal Inspec::Input::NO_VALUE_SET
end
it 'creates an input with a default value' do
registry.register_input('color', 'dummy_profile', default: 'silver')
registry.find_input('color', 'dummy_profile').value.must_equal 'silver'
it 'creates an input with a value' do
registry.find_or_register_input('color', 'dummy_profile', value: 'silver')
registry.find_or_register_input('color', 'dummy_profile').value.must_equal 'silver'
end
end
describe 'creating a profile with a name alias' do
it 'creates a default input on a profile with an alias' do
it 'creates a value input on a profile with an alias' do
registry.register_profile_alias('old_profile', 'new_profile')
registry.register_input('color', 'new_profile', default: 'blue')
registry.find_input('color', 'new_profile').value.must_equal 'blue'
registry.find_input('color', 'old_profile').value.must_equal 'blue'
registry.find_or_register_input('color', 'new_profile', value: 'blue')
registry.find_or_register_input('color', 'new_profile').value.must_equal 'blue'
registry.find_or_register_input('color', 'old_profile').value.must_equal 'blue'
end
end
describe 'creating a profile and select it' do
it 'creates a profile with inputs' do
registry.register_input('color', 'dummy_profile', default: 'silver')
registry.register_input('color2', 'dummy_profile', default: 'blue')
registry.register_input('color3', 'dummy_profile', default: 'green')
registry.find_or_register_input('color', 'dummy_profile', value: 'silver')
registry.find_or_register_input('color2', 'dummy_profile', value: 'blue')
registry.find_or_register_input('color3', 'dummy_profile', value: 'green')
registry.list_inputs_for_profile('dummy_profile').size.must_equal 3
end
end
describe 'validate the correct objects are getting created' do
it 'creates a profile with inputs' do
registry.register_input('color', 'dummy_profile', default: 'silver').class.must_equal Inspec::Input
registry.find_or_register_input('color', 'dummy_profile', value: 'silver').class.must_equal Inspec::Input
registry.list_inputs_for_profile('dummy_profile').size.must_equal 1
end
end
describe 'validate find_input method' do
describe 'validate find_or_register_input method' do
it 'find an input which exist' do
input = registry.register_input('color', 'dummy_profile')
input = registry.find_or_register_input('color', 'dummy_profile')
input.value = 'black'
registry.find_input('color', 'dummy_profile').value.must_equal 'black'
registry.find_or_register_input('color', 'dummy_profile').value.must_equal 'black'
end
end
it 'errors when trying to find an input on an unknown profile' do
input = registry.register_input('color', 'dummy_profile')
ex = assert_raises(Inspec::InputRegistry::ProfileLookupError) { registry.find_input('color', 'unknown_profile') }
ex.message.must_match "Profile 'unknown_profile' does not have any inputs"
# =============================================================== #
# Loading inputs from --attrs
# =============================================================== #
describe '#bind_profile_inputs' do
before do
Inspec::InputRegistry.any_instance.stubs(:validate_inputs_file_readability!)
end
let(:seen_inputs) do
registry.bind_profile_inputs('test_fixture_profile', sources)
inputs = registry.list_inputs_for_profile('test_fixture_profile')
# Flatten Input objects down to their values
inputs.keys.each { |input_name| inputs[input_name] = inputs[input_name].value }
inputs
end
it 'errors when trying to find an unknown input on a known profile' do
input = registry.register_input('color', 'dummy_profile')
ex = assert_raises(Inspec::InputRegistry::InputLookupError) { registry.find_input('unknown_input', 'dummy_profile') }
ex.message.must_match "Profile 'dummy_profile' does not have an input with name 'unknown_input'"
describe 'when no CLI --attrs are specified' do
let(:sources) { { cli_input_files: [] } }
it 'returns an empty hash' do
seen_inputs.must_equal({})
end
end
describe 'when a CLI --attrs option is provided and does not resolve' do
let(:sources) { { cli_input_files: ['nope.jpg'] } }
it 'raises an exception' do
Inspec::SecretsBackend.expects(:resolve).with('nope.jpg').returns(nil)
proc { seen_inputs }.must_raise Inspec::Exceptions::SecretsBackendNotFound
end
end
describe 'when a CLI --attrs option is provided and has no inputs' do
let(:sources) { { cli_input_files: ['empty.yaml'] } }
it 'returns an empty hash' do
secrets = mock
secrets.stubs(:inputs).returns(nil)
Inspec::SecretsBackend.expects(:resolve).with('empty.yaml').returns(secrets)
seen_inputs.must_equal({})
end
end
describe 'when a CLI --attrs file is provided and has inputs' do
let(:sources) { { cli_input_files: ['file1.yaml'] } }
it 'returns a hash containing the inputs' do
fixture_inputs = { foo: 'bar' }
secrets = mock
secrets.stubs(:inputs).returns(fixture_inputs)
Inspec::SecretsBackend.expects(:resolve).with('file1.yaml').returns(secrets)
seen_inputs.must_equal(fixture_inputs)
end
end
describe 'when multiple CLI --attrs option args are provided and one fails' do
let(:sources) { { cli_input_files: ['file1.yaml', 'file2.yaml'] } }
it 'raises an exception' do
secrets = mock
secrets.stubs(:inputs).returns(nil)
Inspec::SecretsBackend.expects(:resolve).with('file1.yaml').returns(secrets)
Inspec::SecretsBackend.expects(:resolve).with('file2.yaml').returns(nil)
proc { seen_inputs }.must_raise Inspec::Exceptions::SecretsBackendNotFound
end
end
describe 'when multiple CLI --attrs option args are provided and one has no inputs' do
let(:sources) { { cli_input_files: ['file1.yaml', 'file2.yaml'] } }
it 'returns a hash containing the inputs from the valid files' do
inputs = { foo: 'bar' }
secrets1 = mock
secrets1.stubs(:inputs).returns(nil)
secrets2 = mock
secrets2.stubs(:inputs).returns(inputs)
Inspec::SecretsBackend.expects(:resolve).with('file1.yaml').returns(secrets1)
Inspec::SecretsBackend.expects(:resolve).with('file2.yaml').returns(secrets2)
seen_inputs.must_equal(inputs)
end
end
describe 'when multiple CLI --attrs option args are provided and all have inputs' do
let(:sources) { { cli_input_files: ['file1.yaml', 'file2.yaml'] } }
it 'returns a hash containing all the inputs' do
options = { attrs: ['file1.yaml', 'file2.yaml'] }
secrets1 = mock
secrets1.stubs(:inputs).returns({ key1: 'value1' })
secrets2 = mock
secrets2.stubs(:inputs).returns({ key2: 'value2' })
Inspec::SecretsBackend.expects(:resolve).with('file1.yaml').returns(secrets1)
Inspec::SecretsBackend.expects(:resolve).with('file2.yaml').returns(secrets2)
seen_inputs.must_equal({ key1: 'value1', key2: 'value2' })
end
end
end
end

View file

@ -4,13 +4,82 @@ require 'helper'
require 'inspec/objects/input'
describe Inspec::Input do
let(:input) { Inspec::Input.new('test_input') }
let(:opts) { { } }
let(:input) { Inspec::Input.new('test_input', opts) }
it 'support storing and returning false' do
input.value = false
input.value.must_equal false
#==============================================================#
# Metadata
#==============================================================#
describe 'setting and reading metadata' do
{
description: 'My favorite attribute',
identifier: 'a_ruby_permitted_name',
required: true,
title: 'how is this different than description',
type: 'Numeric'
}.each do |field, value|
it "should be able to recall the #{field} field" do
opts[field] = value
ipt = Inspec::Input.new('test_attribute', opts)
seen_value = ipt.send(field)
seen_value.must_equal value
end
end
end
#==============================================================#
# Code Generation
#==============================================================#
describe 'to_ruby method' do
it 'generates the code for the input' do
input = Inspec::Input.new('application_port', description: 'The port my application uses', value: 80)
ruby_code = input.to_ruby
ruby_code.must_include "attr_application_port = " # Should assign to a var
ruby_code.must_include "attribute('application_port'" # Should have the DSL call
ruby_code.must_include 'value: 80'
ruby_code.must_include 'default: 80'
ruby_code.must_include "description: 'The port my application uses'"
# Try to eval the code to verify that the generated code was valid ruby.
# Note that the attribute() method is part of the DSL, so we need to
# alter the call into something that can respond - the constructor will do
ruby_code_for_eval = ruby_code.sub(/attribute\(/,'Inspec::Input.new(')
# This will throw exceptions if there is a problem
new_attr = eval(ruby_code_for_eval) # Could use ripper!
new_attr.value.must_equal 80
end
end
# TODO - deprecate this, not sure it is used
describe 'to_hash method' do
it 'generates a similar hash' do
ipt = Inspec::Input.new(
'some_attr',
description: 'The port my application uses',
value: 80,
identifier: 'app_port',
required: false,
type: 'numeric'
)
expected = {
name: 'some_attr',
options: {
description: 'The port my application uses',
value: 80,
identifier: 'app_port',
required: false,
type: 'Numeric', # This gets normalized
}
}
ipt.to_hash.must_equal expected
end
end
#==============================================================#
# Setting Value - One Shot
# (see events_test.rb for overwrite support)
#==============================================================#
describe 'the dummy value used when value is not set' do
it 'returns the actual value, not the dummy object, if one is assigned' do
input.value = 'new_value'
@ -37,7 +106,7 @@ describe Inspec::Input do
end
end
describe 'input with a value set' do
describe 'setting a value in the constructor using value:' do
it 'returns the user-configured value' do
input = Inspec::Input.new('test_input', value: 'some_value')
input.value.must_equal 'some_value'
@ -70,187 +139,20 @@ describe Inspec::Input do
end
end
describe 'validate required method' do
it 'does not error if a value is set' do
input = Inspec::Input.new('test_input', value: 'some_value', required: true)
input.value.must_equal 'some_value'
describe 'setting a value using value=' do
it 'supports storing and returning a value' do
input.value = 'a_value'
input.value.must_equal 'a_value'
end
it 'does not error if a value is specified by value=' do
input = Inspec::Input.new('test_input', required: true)
input.value = 'test_value'
input.value.must_equal 'test_value'
it 'supports storing and returning false' do
input.value = false
input.value.must_equal false
end
it 'returns an error if no value is set' do
# Assigning the cli_command is needed because in check mode, we don't error
# on unset inputs. This is how you tell the input system we are not in
# check mode, apparently.
Inspec::BaseCLI.inspec_cli_command = :exec
input = Inspec::Input.new('test_input', required: true)
ex = assert_raises(Inspec::Input::RequiredError) { input.value }
ex.message.must_match /Input 'test_input' is required and does not have a value./
Inspec::BaseCLI.inspec_cli_command = nil
end
end
describe 'validate value type method' do
let(:opts) { {} }
let(:input) { Inspec::Input.new('test_input', opts) }
it 'validates a string type' do
opts[:type] = 'string'
input.send(:validate_value_type, 'string')
end
it 'returns an error if a invalid string is set' do
opts[:type] = 'string'
ex = assert_raises(Inspec::Input::ValidationError) { input.send(:validate_value_type, 123) }
ex.message.must_match /Input 'test_input' with value '123' does not validate to type 'String'./
end
it 'validates a numeric type' do
opts[:type] = 'numeric'
input.send(:validate_value_type, 123.33)
end
it 'returns an error if a invalid numeric is set' do
opts[:type] = 'numeric'
ex = assert_raises(Inspec::Input::ValidationError) { input.send(:validate_value_type, 'invalid') }
ex.message.must_match /Input 'test_input' with value 'invalid' does not validate to type 'Numeric'./
end
it 'validates a regex type' do
opts[:type] = 'regex'
input.send(:validate_value_type, '/^\d*$/')
end
it 'returns an error if a invalid regex is set' do
opts[:type] = 'regex'
ex = assert_raises(Inspec::Input::ValidationError) { input.send(:validate_value_type, '/(.+/') }
ex.message.must_match "Input 'test_input' with value '/(.+/' does not validate to type 'Regexp'."
end
it 'validates a array type' do
opts[:type] = 'Array'
value = [1, 2, 3]
input.send(:validate_value_type, value)
end
it 'returns an error if a invalid array is set' do
opts[:type] = 'Array'
value = { a: 1, b: 2, c: 3 }
ex = assert_raises(Inspec::Input::ValidationError) { input.send(:validate_value_type, value) }
ex.message.must_match /Input 'test_input' with value '{:a=>1, :b=>2, :c=>3}' does not validate to type 'Array'./
end
it 'validates a hash type' do
opts[:type] = 'Hash'
value = { a: 1, b: 2, c: 3 }
input.send(:validate_value_type, value)
end
it 'returns an error if a invalid hash is set' do
opts[:type] = 'hash'
ex = assert_raises(Inspec::Input::ValidationError) { input.send(:validate_value_type, 'invalid') }
ex.message.must_match /Input 'test_input' with value 'invalid' does not validate to type 'Hash'./
end
it 'validates a boolean type' do
opts[:type] = 'boolean'
input.send(:validate_value_type, false)
input.send(:validate_value_type, true)
end
it 'returns an error if a invalid boolean is set' do
opts[:type] = 'boolean'
ex = assert_raises(Inspec::Input::ValidationError) { input.send(:validate_value_type, 'not_true') }
ex.message.must_match /Input 'test_input' with value 'not_true' does not validate to type 'Boolean'./
end
it 'validates a any type' do
opts[:type] = 'any'
input.send(:validate_value_type, false)
input.send(:validate_value_type, true)
input.send(:validate_value_type, 1)
input.send(:validate_value_type, 'bob')
end
end
describe 'validate type method' do
it 'converts regex to Regexp' do
input.send(:validate_type, 'regex').must_equal 'Regexp'
end
it 'returns the same value if there is nothing to clean' do
input.send(:validate_type, 'String').must_equal 'String'
end
it 'returns an error if a invalid type is sent' do
ex = assert_raises(Inspec::Input::TypeError) { input.send(:validate_type, 'dressing') }
ex.message.must_match /Type 'Dressing' is not a valid input type./
end
end
describe 'valid_regexp? method' do
it 'validates a string regex' do
input.send(:valid_regexp?, '/.*/').must_equal true
end
it 'validates a slash regex' do
input.send(:valid_regexp?, /.*/).must_equal true
end
it 'does not vaildate a invalid regex' do
input.send(:valid_regexp?, '/.*(/').must_equal false
end
end
describe 'valid_numeric? method' do
it 'validates a string number' do
input.send(:valid_numeric?, '123').must_equal true
end
it 'validates a float number' do
input.send(:valid_numeric?, 44.55).must_equal true
end
it 'validats a wrong padded number' do
input.send(:valid_numeric?, '00080').must_equal true
end
it 'does not vaildate a invalid number' do
input.send(:valid_numeric?, '55.55.55.5').must_equal false
end
it 'does not vaildate a invalid string' do
input.send(:valid_numeric?, 'one').must_equal false
end
it 'does not vaildate a fraction' do
input.send(:valid_numeric?, '1/2').must_equal false
end
end
describe 'to_ruby method' do
it 'generates the code for the input' do
input = Inspec::Input.new('application_port', description: 'The port my application uses', value: 80)
ruby_code = input.to_ruby
ruby_code.must_include "attr_application_port = " # Should assign to a var
ruby_code.must_include "attribute('application_port'" # Should have the DSL call
ruby_code.must_include 'value: 80'
ruby_code.must_include 'default: 80'
ruby_code.must_include "description: 'The port my application uses'"
# Try to eval the code to verify that the generated code was valid ruby.
# Note that the attribute() method is part of the DSL, so we need to
# alter the call into something that can respond - the constructor will do
ruby_code_for_eval = ruby_code.sub(/attribute\(/,'Inspec::Input.new(')
# This will throw exceptions if there is a problem
new_attr = eval(ruby_code_for_eval) # Could use ripper!
new_attr.value.must_equal 80
it 'supports storing and returning nil' do
input.value = nil
input.value.must_be_nil
end
end
end

View file

@ -0,0 +1,141 @@
require 'helper'
require 'inspec/objects/input'
describe 'type validation' do
let(:opts) { {} }
let(:input) { Inspec::Input.new('test_input', opts) }
#==============================================================#
# Requiredness
#==============================================================#
describe 'enforce_required_validation' do
it 'does not error if a value is set' do
input = Inspec::Input.new('test_input', value: 'some_value', required: true)
input.value.must_equal 'some_value'
end
it 'does not error if a value is specified by value=' do
input = Inspec::Input.new('test_input', required: true)
input.value = 'test_value'
input.value.must_equal 'test_value'
end
it 'returns an error if no value is set' do
# Assigning the cli_command is needed because in check mode, we don't error
# on unset inputs. This is how you tell the input system we are not in
# check mode, apparently.
Inspec::BaseCLI.inspec_cli_command = :exec
input = Inspec::Input.new('test_input', required: true)
ex = assert_raises(Inspec::Input::RequiredError) { input.value }
ex.message.must_match /Input 'test_input' is required and does not have a value./
Inspec::BaseCLI.inspec_cli_command = nil
end
end
#==============================================================#
# Type Validation
#==============================================================#
describe 'enforce_type_validation' do
{
'string' => { good: 'a_string', bad: 123.3, norm: 'String' },
'numeric' => { good: 123, bad: 'not a number', norm: 'Numeric' },
'regex' => { good: /\d+.+/, bad: '/(.+/', norm: 'Regexp' },
'array' => { good: [1, 2, 3], bad: { a: 1, b: 2, c: 3 }, norm: 'Array' },
'hash' => { good: { a: 1, b: 2, c: 3 }, bad: 'i am not a hash', norm: 'Hash' },
'boolean' => { good: true, bad: 'i am not a boolean', norm: 'Boolean' },
}.each do |type, examples|
it "validates a #{type} in the constructor - (good)" do
opts = { type: type, value: examples[:good] }
Inspec::Input.new('test_input', opts) # No exception
end
it "validates a #{type} in the constructor - (bad)" do
opts = { type: type, value: examples[:bad] }
ex = assert_raises(Inspec::Input::ValidationError) { Inspec::Input.new('test_input', opts) }
ex.message.must_include 'test_input'
ex.message.must_include "'#{examples[:bad]}'"
ex.message.must_include "does not validate to type '#{examples[:norm]}'"
end
it "validates a #{type} in value= (good)" do
att = Inspec::Input.new('test_input', type: type)
att.value = examples[:good]
end
it "validates a #{type} in the value= - (bad)" do
att = Inspec::Input.new('test_input', type: type)
ex = assert_raises(Inspec::Input::ValidationError) { att.value = examples[:bad] }
ex.message.must_include 'test_input'
ex.message.must_include "'#{examples[:bad]}'"
ex.message.must_include "does not validate to type '#{examples[:norm]}'"
end
end
it 'validates the Any type' do
Inspec::Input.new('test_input', type: 'any', value: false) # No exception
Inspec::Input.new('test_input', type: 'any', value: true) # No exception
Inspec::Input.new('test_input', type: 'any', value: 'bob') # No exception
Inspec::Input.new('test_input', type: 'any', value: 1) # No exception
end
end
#==============================================================#
# Type Option Validation and Normalization
#==============================================================#
describe 'validate type option' do
it 'converts regex to Regexp' do
opts[:type] = 'regex'
input.type.must_equal 'Regexp'
end
it 'returns the same value if there is nothing to clean' do
opts[:type] = 'String'
input.type.must_equal 'String'
end
it 'returns an error if a invalid type is sent' do
opts[:type] = 'dressing'
ex = assert_raises(Inspec::Input::TypeError) { input }
ex.message.must_match /Type 'Dressing' is not a valid input type./
end
end
describe 'valid_regexp? method' do
it 'validates a string regex' do
input.send(:valid_regexp?, '/.*/').must_equal true
end
it 'validates a slash regex' do
input.send(:valid_regexp?, /.*/).must_equal true
end
it 'does not validate a invalid regex' do
input.send(:valid_regexp?, '/.*(/').must_equal false
end
end
describe 'valid_numeric? method' do
it 'validates a string number' do
input.send(:valid_numeric?, '123').must_equal true
end
it 'validates a float number' do
input.send(:valid_numeric?, 44.55).must_equal true
end
it 'validats a wrong padded number' do
input.send(:valid_numeric?, '00080').must_equal true
end
it 'does not vaildate a invalid number' do
input.send(:valid_numeric?, '55.55.55.5').must_equal false
end
it 'does not validate a invalid string' do
input.send(:valid_numeric?, 'one').must_equal false
end
it 'does not validate a fraction' do
input.send(:valid_numeric?, '1/2').must_equal false
end
end
end

View file

@ -11,15 +11,14 @@ tests = expecteds.keys.map do |test_name|
name: test_name,
expected: expecteds[test_name],
input_via_string: attribute(test_name.to_s, value: "#{test_name}_default"),
input_via_symbol: attribute(test_name, value: "#{test_name}_default"),
}
end
control 'flat' do
tests.each do |info|
describe "#{info[:name]} using string key" do
subject { info[:input_via_string] }
it { should eq info[:expected] }
tests.each do |details|
describe "#{details[:name]} using string key" do
subject { details[:input_via_string] }
it { should eq details[:expected] }
end
end
end

View file

@ -1,47 +0,0 @@
describe 'test the val_string input set in the global inspec.yml' do
subject { attribute('val_string') }
it { should cmp 'test-attr' }
end
describe 'test the val_numeric input set in the global inspec.yml' do
subject { attribute('val_numeric') }
it { should cmp 443 }
end
describe 'test the val_boolean input set in the global inspec.yml' do
subject { attribute('val_boolean') }
it { should cmp true }
end
describe 'test the val_regex input set in the global inspec.yml' do
subject { attribute('val_regex') }
it { should cmp '/^\d*/'}
end
describe 'test the val_array input set in the global inspec.yml' do
subject { attribute('val_array') }
check_array = [ 'a', 'b', 'c' ]
it { should cmp check_array }
end
describe 'test the val_hash input set in the global inspec.yml' do
subject { attribute('val_hash') }
check_hash = { a: true, b: false, c: '123' }
it { should cmp check_hash }
end
describe 'test input when no default or value is set' do
subject { attribute('val_no_default').respond_to?(:fake_method) }
it { should cmp true }
end
describe 'test input with no defualt but has type' do
subject { attribute('val_no_default_with_type').respond_to?(:fake_method) }
it { should cmp true }
end
empty_hash_input = attribute('val_with_empty_hash_default', {})
describe 'test input with default as empty hash' do
subject { empty_hash_input }
it { should cmp 'success' }
end

View file

@ -1,15 +0,0 @@
include_controls 'child_profile_NEW_NAME'
include_controls 'child_profile2' do
control 'test override control on parent using child attribute' do
describe attribute('val_numeric') do
it { should cmp 654321 }
end
end
control 'test override control on parent using parent attribute' do
describe Inspec::InputRegistry.find_input('val_numeric', 'inputs').value do
it { should cmp 443 }
end
end
end

View file

@ -1,6 +0,0 @@
control 'test using the val_numeric_override with a default in the inspec.yml overridden by the secrets file' do
desc 'test the val_numeric_override attr'
describe attribute('val_numeric_override') do
it { should cmp 9999 }
end
end

View file

@ -1,31 +0,0 @@
val_user = attribute('val_user', default: 'alice', description: 'An identification for the user')
val_user_override = attribute('val_user_override', default: 'alice', description: 'An identification for the user')
describe 'reading an input in a file-level definition with a default value' do
subject { val_user }
it { should cmp 'alice' }
end
describe 'reading an input in a file-level definition with a default value and a value in secrets file' do
subject { val_user_override }
it { should cmp 'bob' }
end
control 'test using an input inside a control block as the describe subject' do
desc 'test the val_numeric attr'
describe attribute('val_user') do
it { should cmp 'alice' }
end
end
# test using a input outside controls and as the describe subject
describe attribute('val_user') do
it { should cmp 'alice' }
end
control "test using inputs in the test it's block" do
describe 'alice' do
it { should cmp attribute('val_user') }
end
end

View file

@ -1,3 +0,0 @@
val_user_override: bob
val_numeric_override: 9999
val_with_empty_hash_default: 'success'

View file

@ -1,13 +0,0 @@
---
lockfile_version: 1
depends:
- name: child_profile_NEW_NAME
resolved_source:
url: https://example.com/child_profile.tar.gz
sha256: e39eb85366b272bae98e5eecdfac9f84c50a9ae9dd625fba2ce847268a6c3477
version_constraints: ">= 0"
- name: child_profile2
resolved_source:
url: https://example.com/child_profile2.tar.gz
sha256: f24eb85366b272bae98e5eecdfac9f84c50a9ae9dd625fba2ce847268a6c3477
version_constraints: ">= 0"

View file

@ -1,43 +0,0 @@
name: global-inputs
title: Profile to test inputs in a variety of locations
maintainer: The Authors
copyright: The Authors
copyright_email: you@example.com
license: Apache-2.0
summary: An InSpec Compliance Profile
version: 0.1.0
depends:
- name: child_profile_NEW_NAME
url: https://example.com/child_profile.tar.gz
- url: https://example.com/child_profile2.tar.gz
attributes:
- name: val_numeric
type: numeric
default: 443
- name: val_numeric_override
type: numeric
default: '72.88'
- name: val_string
type: string
default: 'test-attr'
- name: val_boolean
type: boolean
default: true
- name: val_regex
type: regex
default: '/^\d*/'
- name: val_array
type: array
default:
- a
- b
- c
- name: val_hash
type: hash
default:
a: true
b: false
c: '123'
- name: val_no_default
- name: val_no_default_with_type
type: hash

View file

@ -1,4 +0,0 @@
# Demo Compliance Profile
copyright: Demo Company Ltd
license: All rights reserved

View file

@ -1,10 +0,0 @@
describe 'test child attribute when using a profile with a name override' do
subject { attribute('val_numeric') }
it { should cmp '123456' }
end
control 'test child attribute inside a it block when using a profile with a name override' do
describe '123456' do
it { should cmp attribute('val_numeric') }
end
end

View file

@ -1,12 +0,0 @@
name: child_profile
title: Child Profile
maintainer: Demo, Inc.
copyright: Demo, Inc.
copyright_email: support@example.com
license: Apache-2.0
summary: My Profile 1 summary
version: 1.0.0
attributes:
- name: val_numeric
type: numeric
default: 123456

View file

@ -1,4 +0,0 @@
# Demo Compliance Profile
copyright: Demo Company Ltd
license: All rights reserved

View file

@ -1,22 +0,0 @@
describe 'test child attribute when using a profile without a name override' do
subject { attribute('val_numeric') }
it { should cmp 654321 }
end
control 'test override control on parent using child attribute' do
describe 'dummy' do
it { should cmp 9999 }
end
end
control 'test override control on parent using parent attribute' do
describe 'dummy' do
it { should cmp 9999 }
end
end
control 'test child attribute inside a it block when using a profile without a name override' do
describe '654321' do
it { should cmp attribute('val_numeric') }
end
end

View file

@ -1,12 +0,0 @@
name: child_profile2
title: Child Profile2
maintainer: Demo, Inc.
copyright: Demo, Inc.
copyright_email: support@example.com
license: Apache-2.0
summary: My Profile 1 summary
version: 1.0.0
attributes:
- name: val_numeric
type: numeric
default: 654321

View file

@ -0,0 +1,14 @@
control 'child-01-control-01' do
describe attribute('test-01') do
# This is an independent value, inheritance-child-01/test-01
it { should cmp 'value-from-child-01-metadata' }
end
end
control 'child-01-control-02' do
describe attribute('test-02') do
# This value was set by the wrapper, inheritance-child-01/test-02
it { should cmp 'value-from-wrapper-metadata' }
end
end

View file

@ -0,0 +1,17 @@
name: inheritance-child-01
title: Tests inputs being set accross inheritance boundaries
maintainer: InSpec Core Engineering Team
copyright: InSpec Core Engineering Team
copyright_email: inspec@chef.io
license: Apache-2.0
summary: Tests inputs being set accross inheritance boundaries
version: 0.1.0
supports:
platform: os
attributes:
- name: test-01
value: value-from-child-01-metadata
- name: test-02
value: value-from-child-01-metadata

View file

@ -0,0 +1,6 @@
control 'child-02-control-01' do
describe attribute('test-03') do
# This value was set by the wrapper via an alias, inheritance-child-02/test-02
it { should cmp 'value-from-wrapper-metadata' }
end
end

View file

@ -0,0 +1,15 @@
name: inheritance-child-02
title: Tests inputs being set accross inheritance boundaries
maintainer: InSpec Core Engineering Team
copyright: InSpec Core Engineering Team
copyright_email: inspec@chef.io
license: Apache-2.0
summary: Tests inputs being set accross inheritance boundaries
version: 0.1.0
supports:
platform: os
attributes:
# Set a value in an aliased child profile
- name: test-03
value: value-from-child-02-metadata

View file

@ -0,0 +1,14 @@
# inheritance-child-01 is a simple dependency
include_controls('inheritance-child-01')
# inheritance-child-02 is an aliased dependency
include_controls('inheritance-child-02-aliased')
control 'wrapper-control-01' do
describe attribute('test-01') do
# This is an independent value, inheritance-wrapper/test-01
it { should cmp 'value-from-wrapper-metadata' }
end
input_object('test-01')
end

View file

@ -0,0 +1,32 @@
name: inheritance-wrapper
title: Tests inputs being set accross inheritance boundaries
maintainer: InSpec Core Engineering Team
copyright: InSpec Core Engineering Team
copyright_email: inspec@chef.io
license: Apache-2.0
summary: Tests inputs being set accross inheritance boundaries
version: 0.1.0
supports:
platform: os
depends:
- path: ../child-01
# Assign this one a name, which does not match the name in child-02/inspec.yml
# This should cause an alias record to be created in InputRegistry
- name: inheritance-child-02-aliased
path: ../child-02
attributes:
# test-01 is set in both wrapper metadata and child-01 metadata
# and tested for each profile's idea of the value
# All profiles are namespaced, so they exist independently.
- name: test-01
value: value-from-wrapper-metadata
# Set a value in a child profile from a wrapper profile
- name: test-02
profile: inheritance-child-01
value: value-from-wrapper-metadata
# Set a value in an aliased child profile
- name: test-03
profile: inheritance-child-02-aliased
value: value-from-wrapper-metadata

View file

@ -0,0 +1 @@
attribute('a_required_input')

View file

@ -7,6 +7,6 @@ license: Apache-2.0
summary: An InSpec Compliance Profile
version: 0.1.0
attributes:
- name: username
- name: a_required_input
type: string
required: true

View file

@ -0,0 +1,29 @@
# This should simply not error
unless attribute('test-01') == 'test-01'
raise 'Failed bare input access'
end
describe attribute('test-02') do
it { should cmp 'test-02' }
end
describe 'mentioning an input in a bare describe block as a redirected subject' do
subject { attribute('test-03') }
it { should cmp 'test-03' }
end
control 'test using an input inside a control block as the describe subject' do
desc 'test the val_numeric attr'
describe attribute('test-04') do
it { should cmp 'test-04' }
end
end
control "test using inputs in the test its block" do
describe 'test-05' do
it { should cmp attribute('test-05') }
end
end
# TODO: add test for OR

View file

@ -0,0 +1,20 @@
name: input-scoping
title: Profile to test reading attributes in a variety of scopes
maintainer: Chef InSpec team
copyright: Chef InSpec team
copyright_email: inspec@chef.io
license: Apache-2.0
summary: Profile to test reading attributes in a variety of scopes
version: 0.1.0
attributes:
- name: test-01
value: test-01
- name: test-02
value: test-02
- name: test-03
value: test-03
- name: test-04
value: test-04
- name: test-05
value: test-05

View file

@ -24,12 +24,5 @@ module PluginV2BackCompat
assert_equal Inspec::Plugins::SourceReader, klass
end
def test_get_plugin_v1_base_for_secrets
klass = Inspec.secrets(1)
assert_kind_of Class, klass
assert Inspec::Plugins.const_defined? :Secret
assert_equal Inspec::Plugins::Secret, klass
end
end
end

View file

@ -2,14 +2,14 @@
# copyright: 2017, Chef Software Inc.
require 'helper'
require 'inspec/secrets'
describe Inspec::Runner do
describe '#load_inputs' do
let(:runner) { Inspec::Runner.new({ command_runner: :generic }) }
before do
Inspec::Runner.any_instance.stubs(:validate_inputs_file_readability!)
end
# =============================================================== #
# Reporter Options
# =============================================================== #
describe 'confirm reporter defaults to cli' do
it 'defaults to cli when format and reporter not set' do
@ -36,12 +36,20 @@ describe Inspec::Runner do
end
end
# =============================================================== #
# Exit Codes
# =============================================================== #
describe 'testing runner.run exit codes' do
it 'returns proper exit code when no profile is added' do
proc { runner.run.must_equal 0 }
end
end
# =============================================================== #
# Backend Caching
# =============================================================== #
describe 'when backend caching is enabled' do
it 'returns a backend with caching' do
opts = { command_runner: :generic, backend_cache: true }
@ -64,79 +72,4 @@ describe Inspec::Runner do
backend.backend.cache_enabled?(:command).must_equal false
end
end
describe 'when no input files are specified' do
it 'returns an empty hash' do
options = {}
runner.load_inputs(options).must_equal({})
end
end
describe 'when an input file is provided and does not resolve' do
it 'raises an exception' do
options = { input_file: ['nope.jpg'] }
Inspec::SecretsBackend.expects(:resolve).with('nope.jpg').returns(nil)
proc { runner.load_inputs(options) }.must_raise Inspec::Exceptions::SecretsBackendNotFound
end
end
describe 'when an input file is provided and has no inputs' do
it 'returns an empty hash' do
secrets = mock
secrets.stubs(:inputs).returns(nil)
options = { input_file: ['empty.yaml'] }
Inspec::SecretsBackend.expects(:resolve).with('empty.yaml').returns(secrets)
runner.load_inputs(options).must_equal({})
end
end
describe 'when an input file is provided and has inputs' do
it 'returns a hash containing the inputs' do
options = { input_file: ['file1.yaml'] }
inputs = { foo: 'bar' }
secrets = mock
secrets.stubs(:inputs).returns(inputs)
Inspec::SecretsBackend.expects(:resolve).with('file1.yaml').returns(secrets)
runner.load_inputs(options).must_equal(inputs)
end
end
describe 'when multiple input files are provided and one fails' do
it 'raises an exception' do
options = { input_file: ['file1.yaml', 'file2.yaml'] }
secrets = mock
secrets.stubs(:inputs).returns(nil)
Inspec::SecretsBackend.expects(:resolve).with('file1.yaml').returns(secrets)
Inspec::SecretsBackend.expects(:resolve).with('file2.yaml').returns(nil)
proc { runner.load_inputs(options) }.must_raise Inspec::Exceptions::SecretsBackendNotFound
end
end
describe 'when multiple input files are provided and one has no inputs' do
it 'returns a hash containing the inputs from the valid files' do
options = { input_file: ['file1.yaml', 'file2.yaml'] }
inputs = { foo: 'bar' }
secrets1 = mock
secrets1.stubs(:inputs).returns(nil)
secrets2 = mock
secrets2.stubs(:inputs).returns(inputs)
Inspec::SecretsBackend.expects(:resolve).with('file1.yaml').returns(secrets1)
Inspec::SecretsBackend.expects(:resolve).with('file2.yaml').returns(secrets2)
runner.load_inputs(options).must_equal(inputs)
end
end
describe 'when multiple input files are provided and all have inputs' do
it 'returns a hash containing all the inputs' do
options = { input_file: ['file1.yaml', 'file2.yaml'] }
secrets1 = mock
secrets1.stubs(:inputs).returns({ key1: 'value1' })
secrets2 = mock
secrets2.stubs(:inputs).returns({ key2: 'value2' })
Inspec::SecretsBackend.expects(:resolve).with('file1.yaml').returns(secrets1)
Inspec::SecretsBackend.expects(:resolve).with('file2.yaml').returns(secrets2)
runner.load_inputs(options).must_equal({ key1: 'value1', key2: 'value2' })
end
end
end
end