lib/inspec/input_registry.rb in inspec-4.1.4.preview vs lib/inspec/input_registry.rb in inspec-4.2.0.preview

- old
+ new

@@ -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 + 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? - # 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 initialize + # Keyed on String profile_name => Hash of String input_name => Input object + @inputs_by_profile = {} - def self.register_input(name, profile, options = {}) - instance.register_input(name, profile, options) + # this is a list of optional profile name overrides set in the inspec.yml + @profile_aliases = {} end - def self.register_profile_alias(name, alias_name) - instance.register_profile_alias(name, alias_name) + #-------------------------------------------------------------# + # Support for Profiles + #-------------------------------------------------------------# + + def register_profile_alias(name, alias_name) + @profile_aliases[name] = alias_name end - def self.list_inputs_for_profile(profile) - instance.list_inputs_for_profile(profile) + def list_inputs_for_profile(profile) + inputs_by_profile[profile] = {} unless profile_known?(profile) + inputs_by_profile[profile] end - def initialize - # this is a collection of profiles which have a value of input objects - @list = {} + #-------------------------------------------------------------# + # Support for Individual Inputs + #-------------------------------------------------------------# - # this is a list of optional profile name overrides set in the inspec.yml - @profile_aliases = {} + 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 - 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" + # 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 + #-------------------------------------------------------------# - 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}'" + # 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 - 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) + 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 register_profile_alias(name, alias_name) - @profile_aliases[name] = alias_name + 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 list_inputs_for_profile(profile) - list[profile] = {} unless profile_exist?(profile) - list[profile] + 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