lib/berkshelf/installer.rb in berkshelf-3.0.0.beta6 vs lib/berkshelf/installer.rb in berkshelf-3.0.0.beta7

- old
+ new

@@ -1,173 +1,162 @@ require 'berkshelf/api-client' module Berkshelf class Installer - extend Forwardable - attr_reader :berksfile + attr_reader :lockfile attr_reader :downloader - def_delegator :berksfile, :lockfile - # @param [Berkshelf::Berksfile] berksfile def initialize(berksfile) @berksfile = berksfile + @lockfile = berksfile.lockfile @downloader = Downloader.new(berksfile) end def build_universe berksfile.sources.collect do |source| Thread.new do begin + Berkshelf.formatter.msg("Fetching cookbook index from #{source.uri}...") source.build_universe rescue Berkshelf::APIClientError => ex Berkshelf.formatter.warn "Error retrieving universe from source: #{source}" Berkshelf.formatter.warn " * [#{ex.class}] #{ex}" end end end.map(&:join) end - # @option options [Array<String>, String] cookbooks - # # @return [Array<Berkshelf::CachedCookbook>] - def run(options = {}) - dependencies = lockfile_reduce(berksfile.dependencies(options.slice(:except, :only))) - resolver = Resolver.new(berksfile, dependencies) - lock_deps = [] + def run + reduce_lockfile! - dependencies.each do |dependency| - if dependency.scm_location? - Berkshelf.formatter.fetch(dependency) - downloader.download(dependency) - end + cookbooks = if lockfile.trusted? + install_from_lockfile + else + install_from_universe + end - next if (cookbook = dependency.cached_cookbook).nil? + lockfile.graph.update(cookbooks) + lockfile.update(berksfile.dependencies) + lockfile.save - resolver.add_explicit_dependencies(cookbook) - end + cookbooks + end - Berkshelf.formatter.msg("building universe...") - build_universe + # Install all the dependencies from the lockfile graph. + # + # @return [Array<CachedCookbook>] + # the list of installed cookbooks + def install_from_lockfile + locks = lockfile.graph.locks - cached_cookbooks = resolver.resolve.collect do |name, version, dependency| - lock_deps << dependency - dependency.locked_version ||= Solve::Version.new(version) - if dependency.downloaded? - Berkshelf.formatter.use(dependency.name, dependency.cached_cookbook.version, dependency.location) - dependency.cached_cookbook - else - source = berksfile.sources.find { |source| source.cookbook(name, version) } - remote_cookbook = source.cookbook(name, version) - Berkshelf.formatter.install(name, version, api_source: source, location_type: remote_cookbook.location_type, - location_path: remote_cookbook.location_path) - temp_filepath = downloader.download(name, version) - CookbookStore.import(name, version, temp_filepath) - end + # Only construct the universe if we are going to download things + unless locks.all? { |_, dependency| dependency.downloaded? } + build_universe end - verify_licenses!(lock_deps) - lockfile.update(lock_deps) - cached_cookbooks + locks.sort.collect do |name, dependency| + install(dependency) + end end - # Verify that the licenses of all the cached cookbooks fall in the realm of - # allowed licenses from the Berkshelf Config. + # Resolve and install the dependencies from the "universe", updating the + # lockfile appropiately. # - # @param [Array<Berkshelf::Dependencies>] dependencies - # - # @raise [Berkshelf::LicenseNotAllowed] - # if the license is not permitted and `raise_license_exception` is true - def verify_licenses!(dependencies) - licenses = Array(Berkshelf.config.allowed_licenses) - return if licenses.empty? + # @return [Array<CachedCookbook>] + # the list of installed cookbooks + def install_from_universe + dependencies = lockfile.graph.locks.values + berksfile.dependencies + dependencies = dependencies.inject({}) do |hash, dependency| + # Fancy way of ensuring no duplicate dependencies are used... + hash[dependency.name] ||= dependency + hash + end.values - dependencies.each do |dependency| - next if dependency.location.is_a?(Berkshelf::PathLocation) - cached = dependency.cached_cookbook + resolver = Resolver.new(berksfile, dependencies) - begin - unless licenses.include?(cached.metadata.license) - raise Berkshelf::LicenseNotAllowed.new(cached) - end - rescue Berkshelf::LicenseNotAllowed => e - if Berkshelf.config.raise_license_exception - FileUtils.rm_rf(cached.path) - raise - end + # Download all SCM locations first, since they might have additional + # constraints that we don't yet know about + dependencies.select(&:scm_location?).each do |dependency| + Berkshelf.formatter.fetch(dependency) + dependency.download + end - Berkshelf.ui.warn(e.to_s) + # Unlike when installing from the lockfile, we _always_ need to build + # the universe when installing from the universe... duh + build_universe + + # Add any explicit dependencies for already-downloaded cookbooks (like + # path locations) + dependencies.each do |dependency| + if cookbook = dependency.cached_cookbook + resolver.add_explicit_dependencies(cookbook) end end + + resolver.resolve.sort.collect do |dependency| + install(dependency) + end end - private + # Install a specific dependency. + # + # @param [Dependency] + # the dependency to install + # @return [CachedCookbook] + # the installed cookbook + def install(dependency) + if dependency.downloaded? + Berkshelf.formatter.use(dependency) + dependency.cached_cookbook + else + name, version = dependency.name, dependency.locked_version.to_s + source = berksfile.source_for(name, version) + cookbook = source.cookbook(name, version) - # Returns an instance of `Berkshelf::Dependency` with an equality constraint matching - # the locked version of the dependency in the lockfile. - # - # If no matching dependency is found in the lockfile then nil is returned. - # - # @param [Berkshelf:Dependency] dependency - # - # @return [Berkshelf::Dependency, nil] - def dependency_from_lockfile(dependency) - locked = lockfile.find(dependency) + Berkshelf.formatter.install(source, cookbook) - return nil unless locked - - # If there's a locked_version, make sure it's still satisfied - # by the constraint - if locked.locked_version - unless dependency.version_constraint.satisfies?(locked.locked_version) - raise Berkshelf::OutdatedDependency.new(locked, dependency) - end - end - - locked.version_constraint = Solve::Constraint.new("= #{locked.locked_version}") - locked + stash = downloader.download(name, version) + CookbookStore.import(name, version, stash) end + end - # Merge the locked dependencies against the given dependencies. - # - # For each the given dependencies, check if there's a locked version that - # still satisfies the version constraint. If it does, "lock" that dependency - # because we should just use the locked version. - # - # If a locked dependency exists, but doesn't satisfy the constraint, raise a - # {Berkshelf::OutdatedDependency} and tell the user to run update. - # - # Never use the locked constraint for a dependency with a {PathLocation} - # - # @param [Array<Berkshelf::Dependency>] dependencies - # - # @return [Array<Berkshelf::Dependency>] - def lockfile_reduce(dependencies = []) - {}.tap do |h| - (dependencies + lockfile.dependencies).each do |dependency| - next if h.has_key?(dependency.name) + private - if dependency.path_location? - result = dependency - else - result = dependency_from_lockfile(dependency) || dependency - end + # Iterate over each top-level dependency defined in the lockfile and + # check if that dependency is still defined in the Berksfile. + # + # If the dependency is no longer present in the Berksfile, it is "safely" + # removed using {Lockfile#unlock} and {Lockfile#remove}. This prevents + # the lockfile from "leaking" dependencies when they have been removed + # from the Berksfile, but still remained locked in the lockfile. + # + # If the dependency exists, a constraint comparison is conducted to verify + # that the locked dependency still satisifes the original constraint. This + # handles the edge case where a user has updated or removed a constraint + # on a dependency that already existed in the lockfile. + # + # @raise [OutdatedDependency] + # if the constraint exists, but is no longer satisifed by the existing + # locked version + # + # @return [Array<Dependency>] + def reduce_lockfile! + lockfile.dependencies.each do |dependency| + if berksfile.dependencies.map(&:name).include?(dependency.name) + locked = lockfile.graph.find(dependency) + next if locked.nil? - h[result.name] = result + unless dependency.version_constraint.satisfies?(locked.version) + raise OutdatedDependency.new(locked, dependency) end - end.values + else + lockfile.unlock(dependency) + end end - # The list of dependencies "locked" by the lockfile. - # - # @return [Array<Berkshelf::Dependency>] - # the list of dependencies in this lockfile - def locked_dependencies - lockfile.dependencies - end - - def reduce_scm_locations(dependencies) - dependencies.select { |dependency| SCM_LOCATIONS.include?(dependency.class.location_key) } - end + lockfile.save + end end end