require "forwardable" unless defined?(Forwardable) require_relative "config" require_relative "logger" require_relative "product_reader" require_relative "product_relationship" require_relative "strategy/environment" require_relative "strategy/file" require_relative "strategy/argument" require_relative "strategy/prompt" require_relative "strategy/provided_value" module LicenseAcceptance ACCEPT = "accept".freeze ACCEPT_SILENT = "accept-silent".freeze ACCEPT_NO_PERSIST = "accept-no-persist".freeze class Acceptor extend Forwardable include Logger attr_reader :config, :product_reader, :env_strategy, :file_strategy, :arg_strategy, :prompt_strategy, :provided_strategy def initialize(opts = {}) @config = Config.new(opts) Logger.initialize(config.logger) @product_reader = ProductReader.new product_reader.read @env_strategy = Strategy::Environment.new(ENV) @file_strategy = Strategy::File.new(config) @arg_strategy = Strategy::Argument.new(ARGV) @prompt_strategy = Strategy::Prompt.new(config) @provided_strategy = Strategy::ProvidedValue.new(opts.fetch(:provided, nil)) @acceptance_value = nil end def_delegator :@config, :output # For applications that just need simple logic to handle a failed license acceptance flow we include this small # wrapper. Apps with more complex logic (like logging to a logging engine) should call the non-bang version and # handle the exception. def check_and_persist!(product_id, version) check_and_persist(product_id, version) rescue LicenseNotAcceptedError => e output.puts "#{e.product.pretty_name} cannot execute without accepting the license" exit 172 end def check_and_persist(product_id, version) if accepted_no_persist? logger.debug("Chef License accepted with no persistence") @acceptance_value = ACCEPT_NO_PERSIST return true end product_relationship = product_reader.lookup(product_id, version) missing_licenses = file_strategy.accepted?(product_relationship) # They have already accepted all licenses and stored their acceptance in the persistent files if missing_licenses.empty? logger.debug("All licenses present") @acceptance_value = ACCEPT return true end if accepted? || accepted_silent? if config.persist errs = file_strategy.persist(product_relationship, missing_licenses) if errs.empty? output_num_persisted(missing_licenses.size) unless accepted_silent? else output_persist_failed(errs) end end @acceptance_value = accepted_silent? ? ACCEPT_SILENT : ACCEPT return true end if acceptance_value_provided? value = provided_strategy.value || env_strategy.value || arg_strategy.value output.puts("Unrecognized license acceptance value '#{value}', expected one of: '#{ACCEPT}', '#{ACCEPT_SILENT}', '#{ACCEPT_NO_PERSIST}'") raise LicenseNotAcceptedError.new(product_relationship.parent, missing_licenses) end return true if output.isatty && accepted_license_prompt?(product_relationship, missing_licenses) raise LicenseNotAcceptedError.new(product_relationship.parent, missing_licenses) end def self.check_and_persist!(product_id, version, opts = {}) new(opts).check_and_persist!(product_id, version) end def self.check_and_persist(product_id, version, opts = {}) new(opts).check_and_persist(product_id, version) end def accepted_license_prompt?(product_relationship, missing_licenses) prompt_strategy.request(missing_licenses) do # We have to infer the acceptance value if they use the prompt to accept if config.persist @acceptance_value = ACCEPT file_strategy.persist(product_relationship, missing_licenses) else @acceptance_value = ACCEPT_NO_PERSIST [] end end end # Check whether the specified product requires license acceptance for the given version. def license_required?(mixlib_name, version) product = product_reader.lookup_by_mixlib(mixlib_name) return false if product.nil? # If they don't pass a version we assume they want latest # All versions in all channels require license acceptance return true if %w{latest unstable current stable}.include?(version.to_s) || version.nil? Gem::Version.new(version) >= Gem::Version.new(product.license_required_version) end # Some callers only know about mixlib names so we need a way for them to get the product # id as this library knows it. def id_from_mixlib(mixlib_name) product = product_reader.lookup_by_mixlib(mixlib_name) return nil if product.nil? product.id end # Return the value that was matched ("accept", "accept-no-persist", etc.). Used by callers so they do not # have to know the precedence order between provided/environment/argument. Can just get back the value # that was used. Value is only guaranteed to be set after calling check_and_persist. def acceptance_value @acceptance_value end def accepted? provided_strategy.accepted? || env_strategy.accepted? || arg_strategy.accepted? end # no-persist is silent too def accepted_no_persist? provided_strategy.no_persist? || env_strategy.no_persist? || arg_strategy.no_persist? end # persist but be silent like no-persist def accepted_silent? provided_strategy.silent? || env_strategy.silent? || arg_strategy.silent? end def acceptance_value_provided? provided_strategy.value? || env_strategy.value? || arg_strategy.value? end # In the case where users accept with a command line argument or environment variable # we still want to output the fact that the filesystem was changed. def output_num_persisted(count) s = count > 1 ? "s" : "" output.puts <<~EOM #{Strategy::Prompt::BORDER} #{Strategy::Prompt::CHECK} #{count} product license#{s} accepted. #{Strategy::Prompt::BORDER} EOM end def output_persist_failed(errs) output.puts <<~EOM #{Strategy::Prompt::BORDER} #{Strategy::Prompt::CHECK} Product license accepted. Could not persist acceptance:\n\t* #{errs.map(&:message).join("\n\t* ")} #{Strategy::Prompt::BORDER} EOM end end class LicenseNotAcceptedError < RuntimeError attr_reader :product def initialize(product, missing_licenses) @product = product msg = "Missing licenses for the following:\n* " + missing_licenses.map(&:id).join("\n* ") super(msg) end end end