File Manager
Viewing File: resolver.rb
require "inspec/log"
require "inspec/errors"
module Inspec
#
# Inspec::Resolver is a simple dependency resolver. Unlike Bundler
# or Berkshelf, it does not attempt to resolve each named dependency
# to a single version. Rather, it traverses down the dependency tree
# and:
#
# - Fetches the dependency from the source
# - Checks the presence of cycles, and
# - Checks that the specified dependency source satisfies the
# specified version constraint
#
# The full dependency tree is then available for the loader, which
# will provide the isolation necessary to support multiple versions
# of the same profile being used at runtime.
#
# Currently the fetching happens somewhat lazily depending on the
# implementation of the fetcher being used.
#
class Resolver
# Here deps is an Array of Hashes
def self.resolve(dependencies, cache, working_dir, backend)
reqs = dependencies.map do |dep|
req = Inspec::Requirement.from_metadata(dep, cache, cwd: working_dir, backend: backend)
req || raise("Cannot initialize dependency: #{req}")
end
new.resolve(reqs)
end
def detect_duplicates(deps, top_level, path_string)
seen_items_local = []
deps.each do |dep|
if seen_items_local.include?(dep.name)
problem_cookbook = if top_level
"the inspec.yml for this profile."
else
"the dependency information for #{path_string.split(" ").last}"
end
raise Inspec::DuplicateDep, "The dependency #{dep.name} is listed twice in #{problem_cookbook}"
else
seen_items_local << dep.name
end
end
end
# Here deps is an Array of Inspec::Requirement
def resolve(deps, top_level = true, seen_items = {}, path_string = "") # rubocop:disable Metrics/AbcSize
graph = {}
if top_level
Inspec::Log.debug("Starting traversal of dependencies #{deps.map(&:to_s)}")
else
Inspec::Log.debug("Traversing dependency tree of transitive dependency #{deps.map(&:name)}")
end
detect_duplicates(deps, top_level, path_string)
deps.each do |dep|
# Calling dep.resolved_source forces a fetch. Handle any airgap chicanery early.
if Inspec::Config.cached[:airgap]
begin
dep.resolved_source
rescue Inspec::FetcherFailure
Inspec::Log.debug("Failed to fetch #{dep.name}, falling back to archives if possible")
retry if fallback_to_archive_on_fetch_failure(dep)
end
end
new_seen_items = seen_items.dup
new_path_string = if path_string.empty?
dep.name
else
path_string + " -> #{dep.name}"
end
raise Inspec::CyclicDependencyError, "Dependency #{dep} would cause a dependency cycle (#{new_path_string})" if new_seen_items.key?(dep.resolved_source)
new_seen_items[dep.resolved_source] = true
unless dep.source_satisfies_spec?
raise Inspec::UnsatisfiedVersionSpecification, "The profile #{dep.name} from #{dep.resolved_source} has a version #{dep.source_version} which doesn't match #{dep.version_constraints}"
end
Inspec::Log.debug("Adding dependency #{dep.name} (#{dep.resolved_source})")
graph[dep.name] = dep
unless dep.dependencies.empty?
resolve(dep.dependencies, false, new_seen_items.dup, new_path_string)
end
end
Inspec::Log.debug("Dependency traversal complete.") if top_level
graph
end
def fallback_to_archive_on_fetch_failure(dep)
# This facility is intended to handle situations in which
# the failing dependency *is* available in an archive that we have
# available as a local dependency. We just need to find the archive and
# alter the fetcher to refer to information in the archive.
# Note that the vendor cache already should have the archive inflated
# for this to work (see warm_cache_from_archives() from profile_vendor.rb)
# Refs 4727
# This is where any existing archives should have been inflated -
# that is, this is the vendor cache. Each archive would have a lockfile.
cache_path = dep.cache.path
worth_retrying = false
Dir["#{cache_path}/*/inspec.lock"].each do |lockfile_path|
lockfile = Inspec::Lockfile.from_file(lockfile_path)
dep_set = Inspec::DependencySet.from_lockfile(lockfile, dep.opts)
dep2 = dep_set.dep_list[dep.name]
next unless dep2
if dep.opts.key?(:compliance)
# This is ugly. The compliance fetcher works differently than the others,
# and fails at the resolve stage, not the fetch stage. That means we can't
# tweak the fetcher, we have to tweak the deps opts themselves.
dep.opts[:sha256] = dep2.opts[:sha256]
worth_retrying = true
else
# All other fetchers can be generalized, because they will survive their constructor.
fetcher = dep.fetcher.fetcher # Not the CachedFetcher, but its fetcher
made_a_change = fetcher.update_from_opts(dep2.opts)
end
worth_retrying ||= made_a_change
end
worth_retrying
end
end
end