Class: Inspec::Resolver
- Inherits:
-
Object
- Object
- Inspec::Resolver
- Defined in:
- lib/inspec/dependencies/resolver.rb
Overview
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 Method Summary collapse
-
.resolve(dependencies, cache, working_dir, backend) ⇒ Object
Here deps is an Array of Hashes.
Instance Method Summary collapse
- #detect_duplicates(deps, top_level, path_string) ⇒ Object
- #fallback_to_archive_on_fetch_failure(dep) ⇒ Object
-
#resolve(deps, top_level = true, seen_items = {}, path_string = "") ⇒ Object
Here deps is an Array of Inspec::Requirement.
Class Method Details
.resolve(dependencies, cache, working_dir, backend) ⇒ Object
Here deps is an Array of Hashes
25 26 27 28 29 30 31 |
# File 'lib/inspec/dependencies/resolver.rb', line 25 def self.resolve(dependencies, cache, working_dir, backend) reqs = dependencies.map do |dep| req = Inspec::Requirement.(dep, cache, cwd: working_dir, backend: backend) req || raise("Cannot initialize dependency: #{req}") end new.resolve(reqs) end |
Instance Method Details
#detect_duplicates(deps, top_level, path_string) ⇒ Object
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/inspec/dependencies/resolver.rb', line 33 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 |
#fallback_to_archive_on_fetch_failure(dep) ⇒ Object
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/inspec/dependencies/resolver.rb', line 96 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 = 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] = 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 ||= made_a_change end end |
#resolve(deps, top_level = true, seen_items = {}, path_string = "") ⇒ Object
Here deps is an Array of Inspec::Requirement
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/inspec/dependencies/resolver.rb', line 50 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 |