class Inspec::InputRegistry
The InputRegistry's responsibilities include:
- maintaining a list of Input objects that are bound to profiles - assisting in the lookup and creation of Inputs
Attributes
Public Class Methods
# File lib/inspec/input_registry.rb, line 36 def initialize # 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 = {} # Upon creation, activate all input plugins activators = Inspec::Plugin::V2::Registry.instance.find_activators(plugin_type: :input) @plugins = activators.map do |activator| activator.activate! activator.implementation_class.new end # Activate caching for inputs by default @cache_inputs = true end
Public Instance Methods
Used in testing
# File lib/inspec/input_registry.rb, line 339 def __reset @inputs_by_profile = {} @profile_aliases = {} end
This method is called by the Profile
as soon as it has enough context to allow binding inputs to it.
# File lib/inspec/input_registry.rb, line 146 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]) bind_inputs_from_cli_args(profile_name, sources[:cli_input_arg]) end
# File lib/inspec/input_registry.rb, line 82 def find_or_register_input(input_name, profile_name, options = {}) input_name = input_name.to_s profile_name = profile_name.to_s options[:event].value = Thor::CoreExt::HashWithIndifferentAccess.new(options[:event].value) if options[:event]&.value.is_a?(Hash) if profile_alias?(profile_name) && !profile_aliases[profile_name].nil? alias_name = profile_name profile_name = profile_aliases[profile_name] handle_late_arriving_alias(alias_name, profile_name) if profile_known?(alias_name) end # Find or create the input inputs_by_profile[profile_name] ||= {} if inputs_by_profile[profile_name].key?(input_name) && cache_inputs inputs_by_profile[profile_name][input_name].update(options) else inputs_by_profile[profile_name][input_name] = Inspec::Input.new(input_name, options) poll_plugins_for_update(profile_name, input_name) 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.
# File lib/inspec/input_registry.rb, line 126 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
Returns an Array of input names. This includes input names that plugins may be able to fetch, but have not actually been mentioned in the control code.
# File lib/inspec/input_registry.rb, line 72 def list_potential_input_names_for_profile(profile_name) input_names_from_dsl = inputs_by_profile[profile_name].keys input_names_from_plugins = plugins.map { |plugin| plugin.list_inputs(profile_name) } (input_names_from_dsl + input_names_from_plugins).flatten.uniq end
# File lib/inspec/input_registry.rb, line 105 def poll_plugins_for_update(profile_name, input_name) plugins.each do |plugin| response = plugin.fetch(profile_name, input_name) evt = Inspec::Input::Event.new( action: :fetch, provider: plugin.class.plugin_name, priority: plugin.default_priority, hit: !response.nil? ) evt.value = response unless response.nil? inputs_by_profile[profile_name][input_name].events << evt end end
# File lib/inspec/input_registry.rb, line 59 def register_profile_alias(name, alias_name) @profile_aliases[name] = alias_name end
Private Instance Methods
# File lib/inspec/input_registry.rb, line 160 def bind_inputs_from_cli_args(profile_name, input_list) # TODO: move this into a core plugin return if input_list.nil? return if input_list.empty? # These arrive as an array of "name=value" strings # If the user used a comma, we'll see unfortunately see it as "name=value," pairs input_list.each do |pair| unless pair.include?("=") if pair.end_with?(".yaml") raise ArgumentError, "ERROR: --input is used for individual input values, as --input name=value. Use --input-file to load a YAML file." else raise ArgumentError, "ERROR: An '=' is required when using --input. Usage: --input input_name1=input_value1 input2=value2" end end pair = pair.match(/(.*?)=(.*)/) input_name, input_value = pair[1], pair[2] input_value = parse_cli_input_value(input_name, input_value) evt = Inspec::Input::Event.new( value: input_value, provider: :cli, priority: 50 ) find_or_register_input(input_name, profile_name, event: evt) end end
# File lib/inspec/input_registry.rb, line 239 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
# File lib/inspec/input_registry.rb, line 287 def bind_inputs_from_metadata(profile_name, profile_metadata_obj) # TODO: move this into a core plugin return if profile_metadata_obj.nil? # Metadata files are technically optional if profile_metadata_obj.params.key?(:inputs) raw_inputs = profile_metadata_obj.params[:inputs] elsif profile_metadata_obj.params.key?(:attributes) Inspec.deprecate(:attrs_rename_in_metadata, "Profile: '#{profile_name}'.") raw_inputs = profile_metadata_obj.params[:attributes] else return end unless raw_inputs.is_a?(Array) Inspec::Log.warn "Inputs must be defined as an Array in metadata files. Skipping definition from #{profile_name}." return end raw_inputs.each { |i| handle_raw_input_from_metadata(i, profile_name) } end
# File lib/inspec/input_registry.rb, line 219 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
# File lib/inspec/input_registry.rb, line 308 def handle_raw_input_from_metadata(input_orig, profile_name) input_options = input_orig.dup input_name = input_options.delete(:name) input_options[:provider] = :profile_metadata input_options[:file] = File.join(profile_name, "inspec.yml") input_options[:priority] ||= 30 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], sensitive: input_options[:sensitive], pattern: input_options[:pattern], event: evt ) end
Remove trailing commas, resolve type.
# File lib/inspec/input_registry.rb, line 189 def parse_cli_input_value(input_name, given_value) value = given_value.chomp(",") # Trim trailing comma if any case value when /^true|false$/i value = !!(value =~ /true/i) when /^-?\d+$/ value = value.to_i when /^-?\d+\.\d+$/ value = value.to_f when /^(\[|\{).*(\]|\})$/ # Look for complex values and try to parse them. require "yaml" begin value = YAML.load(value) rescue Psych::SyntaxError => yaml_error # It could be that we just tried to run JSON through the YAML parser. require "json" unless defined?(JSON) begin value = JSON.parse(value) rescue JSON::ParserError => json_error msg = "Unparseable value '#{value}' for --input #{input_name}.\n" msg += "When treated as YAML, error: #{yaml_error.message}\n" msg += "When treated as JSON, error: #{json_error.message}" Inspec::Log.warn msg end end end value end
# File lib/inspec/input_registry.rb, line 271 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