mirror of
https://github.com/inspec/inspec
synced 2024-11-10 23:24:18 +00:00
Merge pull request #4485 from inspec/zenspider/objects-inputs
Split out Inspec::Input functional code from the code generation code.
This commit is contained in:
commit
a0a7917faa
11 changed files with 484 additions and 471 deletions
|
@ -14,31 +14,5 @@ module Inspec
|
|||
class ConfigError::MalformedJson < ConfigError; end
|
||||
class ConfigError::Invalid < ConfigError; end
|
||||
|
||||
class Input
|
||||
class Error < Inspec::Error; end
|
||||
class ValidationError < Error
|
||||
attr_accessor :input_name
|
||||
attr_accessor :input_value
|
||||
attr_accessor :input_type
|
||||
end
|
||||
class TypeError < Error
|
||||
attr_accessor :input_type
|
||||
end
|
||||
class RequiredError < Error
|
||||
attr_accessor :input_name
|
||||
end
|
||||
end
|
||||
|
||||
class InputRegistry
|
||||
class Error < Inspec::Error; end
|
||||
class ProfileLookupError < Error
|
||||
attr_accessor :profile_name
|
||||
end
|
||||
class InputLookupError < Error
|
||||
attr_accessor :profile_name
|
||||
attr_accessor :input_name
|
||||
end
|
||||
end
|
||||
|
||||
class UserInteractionRequired < Error; end
|
||||
end
|
||||
|
|
410
lib/inspec/input.rb
Normal file
410
lib/inspec/input.rb
Normal file
|
@ -0,0 +1,410 @@
|
|||
require "inspec/utils/deprecation"
|
||||
|
||||
# For backwards compatibility during the rename (see #3802),
|
||||
# maintain the Inspec::Attribute namespace for people checking for
|
||||
# Inspec::Attribute::DEFAULT_ATTRIBUTE
|
||||
module Inspec
|
||||
class Attribute
|
||||
# This only exists to create the Inspec::Attribute::DEFAULT_ATTRIBUTE symbol with a class
|
||||
class DEFAULT_ATTRIBUTE; end # rubocop: disable Naming/ClassAndModuleCamelCase
|
||||
end
|
||||
end
|
||||
|
||||
module Inspec
|
||||
class Input
|
||||
|
||||
class Error < Inspec::Error; end
|
||||
class ValidationError < Error
|
||||
attr_accessor :input_name
|
||||
attr_accessor :input_value
|
||||
attr_accessor :input_type
|
||||
end
|
||||
class TypeError < Error
|
||||
attr_accessor :input_type
|
||||
end
|
||||
class RequiredError < Error
|
||||
attr_accessor :input_name
|
||||
end
|
||||
|
||||
#===========================================================================#
|
||||
# Class Input::Event
|
||||
#===========================================================================#
|
||||
|
||||
# TODO: break this out to its own file under inspec/input?
|
||||
# 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 Event
|
||||
|
||||
#===========================================================================#
|
||||
# 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.
|
||||
class NO_VALUE_SET # rubocop: disable Naming/ClassAndModuleCamelCase
|
||||
def initialize(name)
|
||||
@name = name
|
||||
|
||||
# output warn message if we are in a exec call
|
||||
if Inspec::BaseCLI.inspec_cli_command == :exec
|
||||
Inspec::Log.warn(
|
||||
"Input '#{@name}' does not have a value. "\
|
||||
"Use --input-file to provide a value for '#{@name}' or specify a "\
|
||||
"value with `attribute('#{@name}', value: 'somevalue', ...)`."
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def method_missing(*_)
|
||||
self
|
||||
end
|
||||
|
||||
def respond_to_missing?(_, _)
|
||||
true
|
||||
end
|
||||
|
||||
def to_s
|
||||
"Input '#{@name}' does not have a value. Skipping test."
|
||||
end
|
||||
|
||||
def is_a?(klass)
|
||||
if klass == Inspec::Attribute::DEFAULT_ATTRIBUTE
|
||||
Inspec.deprecate(:rename_attributes_to_inputs, "Don't check for `is_a?(Inspec::Attribute::DEFAULT_ATTRIBUTE)`, check for `Inspec::Input::NO_VALUE_SET")
|
||||
true # lie for backward compatibility
|
||||
else
|
||||
super(klass)
|
||||
end
|
||||
end
|
||||
|
||||
def kind_of?(klass)
|
||||
if klass == Inspec::Attribute::DEFAULT_ATTRIBUTE
|
||||
Inspec.deprecate(:rename_attributes_to_inputs, "Don't check for `kind_of?(Inspec::Attribute::DEFAULT_ATTRIBUTE)`, check for `Inspec::Input::NO_VALUE_SET")
|
||||
true # lie for backward compatibility
|
||||
else
|
||||
super(klass)
|
||||
end
|
||||
end
|
||||
end # class NO_VALUE_SET
|
||||
|
||||
#===========================================================================#
|
||||
# Class Inspec::Input
|
||||
#===========================================================================#
|
||||
|
||||
# Validation types for input values
|
||||
VALID_TYPES = %w{
|
||||
String
|
||||
Numeric
|
||||
Regexp
|
||||
Array
|
||||
Hash
|
||||
Boolean
|
||||
Any
|
||||
}.freeze
|
||||
|
||||
# TODO: this is not used anywhere?
|
||||
# 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
|
||||
if @opts.key?(:default)
|
||||
Inspec.deprecate(:attrs_value_replaces_default, "input name: '#{name}'")
|
||||
if @opts.key?(:value)
|
||||
Inspec::Log.warn "Input #{@name} created using both :default and :value options - ignoring :default"
|
||||
@opts.delete(:default)
|
||||
end
|
||||
end
|
||||
|
||||
# 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
|
||||
|
||||
# TODO: is this here just for testing?
|
||||
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!
|
||||
end
|
||||
|
||||
def value
|
||||
enforce_required_validation!
|
||||
current_value
|
||||
end
|
||||
|
||||
def has_value?
|
||||
!current_value.is_a? NO_VALUE_SET
|
||||
end
|
||||
|
||||
#--------------------------------------------------------------------------#
|
||||
# Value Type Coercion
|
||||
#--------------------------------------------------------------------------#
|
||||
|
||||
def to_s
|
||||
"Input #{name} with #{current_value}"
|
||||
end
|
||||
|
||||
#--------------------------------------------------------------------------#
|
||||
# Validation
|
||||
#--------------------------------------------------------------------------#
|
||||
|
||||
private
|
||||
|
||||
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
|
||||
|
||||
proposed_value = current_value
|
||||
if proposed_value.nil? || proposed_value.is_a?(NO_VALUE_SET)
|
||||
error = Inspec::Input::RequiredError.new
|
||||
error.input_name = name
|
||||
raise error, "Input '#{error.input_name}' is required and does not have a value."
|
||||
end
|
||||
end
|
||||
|
||||
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 unless valid_regexp?(proposed_value)
|
||||
elsif type_req == "Numeric"
|
||||
invalid_type = true unless valid_numeric?(proposed_value)
|
||||
elsif type_req == "Boolean"
|
||||
invalid_type = true unless [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_req = abbreviations[type_req] if abbreviations.key?(type_req)
|
||||
unless VALID_TYPES.include?(type_req)
|
||||
error = Inspec::Input::TypeError.new
|
||||
error.input_type = type_req
|
||||
raise error, "Type '#{error.input_type}' is not a valid input type."
|
||||
end
|
||||
@type = type_req
|
||||
end
|
||||
|
||||
def valid_numeric?(value)
|
||||
Float(value)
|
||||
true
|
||||
rescue
|
||||
false
|
||||
end
|
||||
|
||||
def valid_regexp?(value)
|
||||
# check for invalid regex syntax
|
||||
Regexp.new(value)
|
||||
true
|
||||
rescue
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,6 @@
|
|||
require "forwardable"
|
||||
require "singleton"
|
||||
require "inspec/objects/input"
|
||||
require "inspec/input"
|
||||
require "inspec/secrets"
|
||||
require "inspec/exceptions"
|
||||
require "inspec/plugin/v2"
|
||||
|
@ -13,6 +13,15 @@ module Inspec
|
|||
include Singleton
|
||||
extend Forwardable
|
||||
|
||||
class Error < Inspec::Error; end
|
||||
class ProfileLookupError < Error
|
||||
attr_accessor :profile_name
|
||||
end
|
||||
class InputLookupError < Error
|
||||
attr_accessor :profile_name
|
||||
attr_accessor :input_name
|
||||
end
|
||||
|
||||
attr_reader :inputs_by_profile, :profile_aliases, :plugins
|
||||
def_delegator :inputs_by_profile, :each
|
||||
def_delegator :inputs_by_profile, :[]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
module Inspec
|
||||
autoload :Input, "inspec/objects/input"
|
||||
# TODO: these should be namespaced in Objects
|
||||
autoload :Tag, "inspec/objects/tag"
|
||||
autoload :Control, "inspec/objects/control"
|
||||
autoload :Describe, "inspec/objects/describe"
|
||||
|
@ -10,3 +10,5 @@ module Inspec
|
|||
autoload :Test, "inspec/objects/test"
|
||||
autoload :Value, "inspec/objects/value"
|
||||
end
|
||||
|
||||
require "inspec/objects/input" # already defined so you can't autoload
|
||||
|
|
|
@ -1,307 +1,14 @@
|
|||
require "inspec/utils/deprecation"
|
||||
|
||||
# For backwards compatibility during the rename (see #3802),
|
||||
# maintain the Inspec::Attribute namespace for people checking for
|
||||
# Inspec::Attribute::DEFAULT_ATTRIBUTE
|
||||
module Inspec
|
||||
class Attribute
|
||||
# This only exists to create the Inspec::Attribute::DEFAULT_ATTRIBUTE symbol with a class
|
||||
class DEFAULT_ATTRIBUTE; end # rubocop: disable Naming/ClassAndModuleCamelCase
|
||||
end
|
||||
end
|
||||
require "inspec/input"
|
||||
|
||||
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.
|
||||
class NO_VALUE_SET # rubocop: disable Naming/ClassAndModuleCamelCase
|
||||
def initialize(name)
|
||||
@name = name
|
||||
|
||||
# output warn message if we are in a exec call
|
||||
if Inspec::BaseCLI.inspec_cli_command == :exec
|
||||
Inspec::Log.warn(
|
||||
"Input '#{@name}' does not have a value. "\
|
||||
"Use --input-file to provide a value for '#{@name}' or specify a "\
|
||||
"value with `attribute('#{@name}', value: 'somevalue', ...)`."
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def method_missing(*_)
|
||||
self
|
||||
end
|
||||
|
||||
def respond_to_missing?(_, _)
|
||||
true
|
||||
end
|
||||
|
||||
def to_s
|
||||
"Input '#{@name}' does not have a value. Skipping test."
|
||||
end
|
||||
|
||||
def is_a?(klass)
|
||||
if klass == Inspec::Attribute::DEFAULT_ATTRIBUTE
|
||||
Inspec.deprecate(:rename_attributes_to_inputs, "Don't check for `is_a?(Inspec::Attribute::DEFAULT_ATTRIBUTE)`, check for `Inspec::Input::NO_VALUE_SET")
|
||||
true # lie for backward compatibility
|
||||
else
|
||||
super(klass)
|
||||
end
|
||||
end
|
||||
|
||||
def kind_of?(klass)
|
||||
if klass == Inspec::Attribute::DEFAULT_ATTRIBUTE
|
||||
Inspec.deprecate(:rename_attributes_to_inputs, "Don't check for `kind_of?(Inspec::Attribute::DEFAULT_ATTRIBUTE)`, check for `Inspec::Input::NO_VALUE_SET")
|
||||
true # lie for backward compatibility
|
||||
else
|
||||
super(klass)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
# NOTE: due to namespacing, this reopens and extends the existing
|
||||
# Inspec::Input. This should be under Inspec::Objects but that ship
|
||||
# has sailed.
|
||||
|
||||
class Input
|
||||
#===========================================================================#
|
||||
# Class Inspec::Input
|
||||
#===========================================================================#
|
||||
|
||||
# Validation types for input values
|
||||
VALID_TYPES = %w{
|
||||
String
|
||||
Numeric
|
||||
Regexp
|
||||
Array
|
||||
Hash
|
||||
Boolean
|
||||
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
|
||||
if @opts.key?(:default)
|
||||
Inspec.deprecate(:attrs_value_replaces_default, "input name: '#{name}'")
|
||||
if @opts.key?(:value)
|
||||
Inspec::Log.warn "Input #{@name} created using both :default and :value options - ignoring :default"
|
||||
@opts.delete(:default)
|
||||
end
|
||||
end
|
||||
|
||||
# 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!
|
||||
end
|
||||
|
||||
def value
|
||||
enforce_required_validation!
|
||||
current_value
|
||||
end
|
||||
|
||||
def has_value?
|
||||
!current_value.is_a? NO_VALUE_SET
|
||||
end
|
||||
# NOTE: No initialize method or accessors for the reasons listed above
|
||||
|
||||
#--------------------------------------------------------------------------#
|
||||
# Marshalling
|
||||
|
@ -334,94 +41,5 @@ module Inspec
|
|||
res.push "})"
|
||||
res.join("\n")
|
||||
end
|
||||
|
||||
#--------------------------------------------------------------------------#
|
||||
# Value Type Coercion
|
||||
#--------------------------------------------------------------------------#
|
||||
|
||||
def to_s
|
||||
"Input #{name} with #{current_value}"
|
||||
end
|
||||
|
||||
#--------------------------------------------------------------------------#
|
||||
# Validation
|
||||
#--------------------------------------------------------------------------#
|
||||
|
||||
private
|
||||
|
||||
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
|
||||
|
||||
proposed_value = current_value
|
||||
if proposed_value.nil? || proposed_value.is_a?(NO_VALUE_SET)
|
||||
error = Inspec::Input::RequiredError.new
|
||||
error.input_name = name
|
||||
raise error, "Input '#{error.input_name}' is required and does not have a value."
|
||||
end
|
||||
end
|
||||
|
||||
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 unless valid_regexp?(proposed_value)
|
||||
elsif type_req == "Numeric"
|
||||
invalid_type = true unless valid_numeric?(proposed_value)
|
||||
elsif type_req == "Boolean"
|
||||
invalid_type = true unless [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_req = abbreviations[type_req] if abbreviations.key?(type_req)
|
||||
unless VALID_TYPES.include?(type_req)
|
||||
error = Inspec::Input::TypeError.new
|
||||
error.input_type = type_req
|
||||
raise error, "Type '#{error.input_type}' is not a valid input type."
|
||||
end
|
||||
@type = type_req
|
||||
end
|
||||
|
||||
def valid_numeric?(value)
|
||||
Float(value)
|
||||
true
|
||||
rescue
|
||||
false
|
||||
end
|
||||
|
||||
def valid_regexp?(value)
|
||||
# check for invalid regex syntax
|
||||
Regexp.new(value)
|
||||
true
|
||||
rescue
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,7 @@ require "inspec/library_eval_context"
|
|||
require "inspec/control_eval_context"
|
||||
require "inspec/require_loader"
|
||||
require "securerandom"
|
||||
require "inspec/objects/input"
|
||||
require "inspec/input_registry"
|
||||
|
||||
module Inspec
|
||||
class ProfileContext
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
require "inspec/objects/input"
|
||||
require "inspec/input"
|
||||
|
||||
module PkeyReader
|
||||
def read_pkey(filecontent, passphrase)
|
||||
|
|
|
@ -502,4 +502,55 @@ end
|
|||
control.to_hash.must_equal control_hash
|
||||
end
|
||||
end
|
||||
|
||||
describe "Inspec::Input" do
|
||||
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
|
||||
# Should have the DSL call. This should be attribute(), not input(), for the
|
||||
# foreseeable future, to maintain backwards compatibility.
|
||||
ruby_code.must_include "attribute('application_port'"
|
||||
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 input() 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) # rubocop:disable Security/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
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
require "helper"
|
||||
require "inspec/objects/input"
|
||||
require "inspec/input"
|
||||
|
||||
describe "Inspec::Input and Events" do
|
||||
let(:ipt) { Inspec::Input.new("input") }
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
require "helper"
|
||||
require "inspec/objects/input"
|
||||
require "inspec/input"
|
||||
|
||||
describe Inspec::Input do
|
||||
let(:opts) { {} }
|
||||
|
@ -25,57 +25,6 @@ describe Inspec::Input do
|
|||
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
|
||||
# Should have the DSL call. This should be attribute(), not input(), for the
|
||||
# foreseeable future, to maintain backwards compatibility.
|
||||
ruby_code.must_include "attribute('application_port'"
|
||||
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 input() 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) # rubocop:disable Security/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)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
require "helper"
|
||||
require "inspec/objects/input"
|
||||
require "inspec/input"
|
||||
|
||||
describe "type validation" do
|
||||
let(:opts) { {} }
|
||||
|
|
Loading…
Reference in a new issue