puppetlabs/facter-ng

View on GitHub
lib/custom_facts/util/loader.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true

# Load facts on demand.
module LegacyFacter
  module Util
    class Loader
      def initialize(environment_vars = ENV)
        @loaded = []
        @environment_vars = environment_vars
      end

      # Load all resolutions for a single fact.
      #
      # @api public
      # @param name [Symbol]
      def load(fact)
        # Now load from the search path
        shortname = fact.to_s.downcase
        load_env(shortname)

        filename = shortname + '.rb'

        paths = search_path
        paths&.each do |dir|
          # Load individual files
          file = File.join(dir, filename)

          load_file(file) if FileTest.file?(file)
        end
      end

      # Load all facts from all directories.
      #
      # @api public
      def load_all
        return if defined?(@loaded_all)

        load_env

        paths = search_path
        paths&.each do |dir|
          # clean the search path of wrong slashes and backslashes
          dir = dir.gsub(%r{[\/\\]+}, File::SEPARATOR)
          # dir is already an absolute path
          Dir.glob(File.join(dir, '*.rb')).each do |path|
            # exclude dirs that end with .rb
            load_file(path) if FileTest.file?(path)
          end
        end

        @loaded_all = true
      end

      # List directories to search for fact files.
      #
      # Search paths are gathered from the following sources:
      #
      # 1. $LOAD_PATH entries are expanded to absolute paths
      # 2. ENV['FACTERLIB'] is split and used verbatim
      # 3. Entries from Facter.search_path are used verbatim
      #
      # A warning will be generated for paths in Facter.search_path that are not
      # absolute directories.
      #
      # @api public
      # @return [Array<String>]
      def search_path
        search_paths = []
        search_paths += $LOAD_PATH.map { |path| File.expand_path('facter', path) }

        if @environment_vars.include?('FACTERLIB')
          search_paths += @environment_vars['FACTERLIB'].split(File::PATH_SEPARATOR)
        end

        search_paths.delete_if { |path| !valid_search_path?(path) }

        Facter::Options.custom_dir.each do |path|
          if valid_search_path?(path)
            search_paths << path
          else
            LegacyFacter.warn "Excluding #{path} from search path. Fact file paths must be an absolute directory"
          end
        end

        search_paths.delete_if { |path| !File.directory?(path) }

        search_paths.uniq
      end

      private

      # Validate that the given path is valid, ie it is an absolute path.
      #
      # @api private
      # @param path [String]
      # @return [Boolean]
      def valid_search_path?(path)
        Pathname.new(path).absolute?
      end

      # Load a file and record is paths to prevent duplicate loads.
      #
      # @api private
      # @params file [String] The *absolute path* to the file to load
      def load_file(file)
        return if @loaded.include? file

        # We have to specify Kernel.load, because we have a load method.
        begin
          # Store the file path so we don't try to reload it
          @loaded << file
          kernel_load(file)
        rescue ScriptError => e
          # Don't store the path if the file can't be loaded
          # in case it's loadable later on.
          @loaded.delete(file)
          Facter.log_exception(e, "Error loading fact #{file}: #{e.message}")
        rescue StandardError => e
          Facter.log_exception(e, "error while resolving custom facts in #{file} #{e.message}")
        end
      end

      # Load and execute the Ruby program specified in the file. This exists
      # for testing purposes.
      #
      # @api private
      # @return [Boolean]
      def kernel_load(file)
        Kernel.load(file)
      end

      # Load facts from the environment.  If no name is provided,
      # all will be loaded.
      def load_env(fact = nil)
        # Load from the environment, if possible
        @environment_vars.each do |name, value|
          # Skip anything that doesn't match our regex.
          next unless name =~ /^facter_?(\w+)$/i

          env_name = Regexp.last_match(1)

          # If a fact name was specified, skip anything that doesn't
          # match it.
          next if fact && (env_name != fact)

          LegacyFacter.add(Regexp.last_match(1), fact_type: :external) do
            has_weight 1_000_000
            setcode { value }
          end

          # Short-cut, if we are only looking for one value.
          break if fact
        end
      end
    end
  end
end