lib/license_acceptance/acceptor.rb



require "forwardable"
require "license_acceptance/config"
require "license_acceptance/logger"
require "license_acceptance/product_reader"
require "license_acceptance/product_relationship"
require "license_acceptance/file_acceptance"
require "license_acceptance/arg_acceptance"
require "license_acceptance/prompt_acceptance"
require "license_acceptance/env_acceptance"

module LicenseAcceptance
  class Acceptor
    extend Forwardable
    include Logger

    attr_reader :config, :product_reader, :env_acceptance, :file_acceptance, :arg_acceptance, :prompt_acceptance

    def initialize(opts={})
      @config = Config.new(opts)
      Logger.initialize(config.logger)
      @product_reader = ProductReader.new
      @env_acceptance = EnvAcceptance.new
      @file_acceptance = FileAcceptance.new(config)
      @arg_acceptance = ArgAcceptance.new
      @prompt_acceptance = PromptAcceptance.new(config)
    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_name, version)
      check_and_persist(product_name, version)
    rescue LicenseNotAcceptedError
      output.puts "#{product_name} cannot execute without accepting the license"
      exit 172
    end

    def check_and_persist(product_name, version)
      if accepted_no_persist?
        logger.debug("Chef License accepted with no persistence")
        return true
      end

      product_reader.read
      product_relationship = product_reader.lookup(product_name, version)

      missing_licenses = file_acceptance.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")
        return true
      end

      if accepted? || accepted_silent?
        if config.persist
          errs = file_acceptance.persist(product_relationship, missing_licenses)
          if errs.empty?
            output_num_persisted(missing_licenses.size) unless accepted_silent?
          else
            output_persist_failed(errs)
          end
        end
        return true
      elsif config.output.isatty && prompt_acceptance.request(missing_licenses) do
          if config.persist
            file_acceptance.persist(product_relationship, missing_licenses)
          else
            []
          end
        end
        return true
      else
        raise LicenseNotAcceptedError.new(missing_licenses)
      end
    end

    def self.check_and_persist!(product_name, version, opts={})
      new(opts).check_and_persist!(product_name, version)
    end

    def self.check_and_persist(product_name, version, opts={})
      new(opts).check_and_persist(product_name, version)
    end

    def accepted?
      env_acceptance.accepted?(ENV) || arg_acceptance.accepted?(ARGV)
    end

    # no-persist is silent too
    def accepted_no_persist?
      env_acceptance.no_persist?(ENV) || arg_acceptance.no_persist?(ARGV)
    end

    # persist but be silent like no-persist
    def accepted_silent?
      env_acceptance.silent?(ENV) || arg_acceptance.silent?(ARGV)
    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
      #{PromptAcceptance::BORDER}
      #{PromptAcceptance::CHECK} #{count} product license#{s} accepted.
      #{PromptAcceptance::BORDER}
      EOM
    end

    def output_persist_failed(errs)
      output.puts <<~EOM
      #{PromptAcceptance::BORDER}
      #{PromptAcceptance::CHECK} Product license accepted.
      Could not persist acceptance:\n\t* #{errs.map(&:message).join("\n\t* ")}
      #{PromptAcceptance::BORDER}
      EOM
    end

  end

  class LicenseNotAcceptedError < RuntimeError
    def initialize(missing_licenses)
      msg = "Missing licenses for the following:\n* " + missing_licenses.join("\n* ")
      super(msg)
    end
  end

end