diff --git a/lib/puppet/module_tool/applications/installer.rb b/lib/puppet/module_tool/applications/installer.rb index 7ce533073..e47cb80bd 100644 --- a/lib/puppet/module_tool/applications/installer.rb +++ b/lib/puppet/module_tool/applications/installer.rb @@ -1,353 +1,351 @@ require 'open-uri' require 'pathname' require 'fileutils' require 'tmpdir' require 'puppet/forge' require 'puppet/module_tool' require 'puppet/module_tool/shared_behaviors' require 'puppet/module_tool/install_directory' require 'puppet/module_tool/local_tarball' require 'puppet/module_tool/installed_modules' module Puppet::ModuleTool module Applications class Installer < Application include Puppet::ModuleTool::Errors include Puppet::Forge::Errors def initialize(name, install_dir, options = {}) super(options) @action = :install @environment = options[:environment_instance] @ignore_dependencies = forced? || options[:ignore_dependencies] @name = name @install_dir = install_dir Puppet::Forge::Cache.clean @local_tarball = Puppet::FileSystem.exist?(name) if @local_tarball release = local_tarball_source.release @name = release.name options[:version] = release.version.to_s Semantic::Dependency.add_source(local_tarball_source) # If we're operating on a local tarball and ignoring dependencies, we # don't need to search any additional sources. This will cut down on # unnecessary network traffic. unless @ignore_dependencies Semantic::Dependency.add_source(installed_modules_source) Semantic::Dependency.add_source(module_repository) end else Semantic::Dependency.add_source(installed_modules_source) unless forced? Semantic::Dependency.add_source(module_repository) end end def run name = @name.tr('/', '-') version = options[:version] || '>= 0.0.0' results = { :action => :install, :module_name => name, :module_version => version } begin if installed_module = installed_modules[name] unless forced? if Semantic::VersionRange.parse(version).include? installed_module.version results[:result] = :noop results[:version] = installed_module.version return results else changes = Checksummer.run(installed_modules[name].mod.path) rescue [] raise AlreadyInstalledError, :module_name => name, :installed_version => installed_modules[name].version, :requested_version => options[:version] || :latest, :local_changes => changes end end end @install_dir.prepare(name, options[:version] || 'latest') results[:install_dir] = @install_dir.target unless @local_tarball && @ignore_dependencies Puppet.notice "Downloading from #{module_repository.host} ..." end if @ignore_dependencies graph = build_single_module_graph(name, version) else graph = build_dependency_graph(name, version) end unless forced? add_module_name_constraints_to_graph(graph) end installed_modules.each do |mod, release| mod = mod.tr('/', '-') next if mod == name version = release.version unless forced? # Since upgrading already installed modules can be troublesome, # we'll place constraints on the graph for each installed module, # locking it to upgrades within the same major version. - ">=#{version} #{version.major}.x".tap do |range| - graph.add_constraint('installed', mod, range) do |node| - Semantic::VersionRange.parse(range).include? node.version - end + installed_range = ">=#{version} #{version.major}.x" + graph.add_constraint('installed', mod, installed_range) do |node| + Semantic::VersionRange.parse(installed_range).include? node.version end release.mod.dependencies.each do |dep| dep_name = dep['name'].tr('/', '-') - dep['version_requirement'].tap do |range| - graph.add_constraint("#{mod} constraint", dep_name, range) do |node| - Semantic::VersionRange.parse(range).include? node.version - end + range = dep['version_requirement'] + graph.add_constraint("#{mod} constraint", dep_name, range) do |node| + Semantic::VersionRange.parse(range).include? node.version end end end end # Ensure that there is at least one candidate release available # for the target package. if graph.dependencies[name].empty? raise NoCandidateReleasesError, results.merge(:module_name => name, :source => module_repository.host, :requested_version => options[:version] || :latest) end begin Puppet.info "Resolving dependencies ..." releases = Semantic::Dependency.resolve(graph) rescue Semantic::Dependency::UnsatisfiableGraph raise NoVersionsSatisfyError, results.merge(:requested_name => name) end unless forced? # Check for module name conflicts. releases.each do |rel| if installed_module = installed_modules_source.by_name[rel.name.split('-').last] next if installed_module.has_metadata? && installed_module.forge_name.tr('/', '-') == rel.name if rel.name != name dependency = { :name => rel.name, :version => rel.version } end raise InstallConflictError, :requested_module => name, :requested_version => options[:version] || 'latest', :dependency => dependency, :directory => installed_module.path, :metadata => installed_module.metadata end end end Puppet.info "Preparing to install ..." releases.each { |release| release.prepare } Puppet.notice 'Installing -- do not interrupt ...' releases.each do |release| installed = installed_modules[release.name] if forced? || installed.nil? release.install(Pathname.new(results[:install_dir])) else release.install(Pathname.new(installed.mod.modulepath)) end end results[:result] = :success results[:installed_modules] = releases results[:graph] = [ build_install_graph(releases.first, releases) ] rescue ModuleToolError, ForgeError => err results[:error] = { :oneline => err.message, :multiline => err.multiline, } ensure results[:result] ||= :failure end results end private def module_repository @repo ||= Puppet::Forge.new end def local_tarball_source @tarball_source ||= begin Puppet::ModuleTool::LocalTarball.new(@name) rescue Puppet::Module::Error => e raise InvalidModuleError.new(@name, :action => @action, :error => e) end end def installed_modules_source @installed ||= Puppet::ModuleTool::InstalledModules.new(@environment) end def installed_modules installed_modules_source.modules end def build_single_module_graph(name, version) range = Semantic::VersionRange.parse(version) graph = Semantic::Dependency::Graph.new(name => range) releases = Semantic::Dependency.fetch_releases(name) releases.each { |release| release.dependencies.clear } graph << releases end def build_dependency_graph(name, version) Semantic::Dependency.query(name => version) end def build_install_graph(release, installed, graphed = []) graphed << release dependencies = release.dependencies.values.map do |deps| dep = (deps & installed).first unless dep.nil? || graphed.include?(dep) build_install_graph(dep, installed, graphed) end end previous = installed_modules[release.name] previous = previous.version if previous return { :release => release, :name => release.name, :path => release.install_dir.to_s, :dependencies => dependencies.compact, :version => release.version, :previous_version => previous, :action => (previous.nil? || previous == release.version || forced? ? :install : :upgrade), } end include Puppet::ModuleTool::Shared # Return a Pathname object representing the path to the module # release package in the `Puppet.settings[:module_working_dir]`. def get_release_packages get_local_constraints if !forced? && @installed.include?(@module_name) raise AlreadyInstalledError, :module_name => @module_name, :installed_version => @installed[@module_name].first.version, :requested_version => @version || (@conditions[@module_name].empty? ? :latest : :best), :local_changes => Puppet::ModuleTool::Applications::Checksummer.run(@installed[@module_name].first.path) end if @ignore_dependencies && @source == :filesystem @urls = {} @remote = { "#{@module_name}@#{@version}" => { } } @versions = { @module_name => [ { :vstring => @version, :semver => SemVer.new(@version) } ] } else get_remote_constraints(@forge) end @graph = resolve_constraints({ @module_name => @version }) @graph.first[:tarball] = @filename if @source == :filesystem resolve_install_conflicts(@graph) unless forced? # This clean call means we never "cache" the module we're installing, but this # is desired since module authors can easily rerelease modules different content but the same # version number, meaning someone with the old content cached will be very confused as to why # they can't get new content. # Long term we should just get rid of this caching behavior and cleanup downloaded modules after they install # but for now this is a quick fix to disable caching Puppet::Forge::Cache.clean download_tarballs(@graph, @graph.last[:path], @forge) end # # Resolve installation conflicts by checking if the requested module # or one of its dependencies conflicts with an installed module. # # Conflicts occur under the following conditions: # # When installing 'puppetlabs-foo' and an existing directory in the # target install path contains a 'foo' directory and we cannot determine # the "full name" of the installed module. # # When installing 'puppetlabs-foo' and 'pete-foo' is already installed. # This is considered a conflict because 'puppetlabs-foo' and 'pete-foo' # install into the same directory 'foo'. # def resolve_install_conflicts(graph, is_dependency = false) Puppet.debug("Resolving conflicts for #{graph.map {|n| n[:module]}.join(',')}") graph.each do |release| @environment.modules_by_path[options[:target_dir]].each do |mod| if mod.has_metadata? metadata = { :name => mod.forge_name.gsub('/', '-'), :version => mod.version } next if release[:module] == metadata[:name] else metadata = nil end if release[:module] =~ /-#{mod.name}$/ dependency_info = { :name => release[:module], :version => release[:version][:vstring] } dependency = is_dependency ? dependency_info : nil all_versions = @versions["#{@module_name}"].sort_by { |h| h[:semver] } versions = all_versions.select { |x| x[:semver].special == '' } versions = all_versions if versions.empty? latest_version = versions.last[:vstring] raise InstallConflictError, :requested_module => @module_name, :requested_version => @version || "latest: v#{latest_version}", :dependency => dependency, :directory => mod.path, :metadata => metadata end end deps = release[:dependencies] if deps && !deps.empty? resolve_install_conflicts(deps, true) end end end # # Check if a file is a vaild module package. # --- # FIXME: Checking for a valid module package should be more robust and # use the actual metadata contained in the package. 03132012 - Hightower # +++ # def is_module_package?(name) filename = File.expand_path(name) filename =~ /.tar.gz$/ end end end end diff --git a/lib/puppet/module_tool/applications/upgrader.rb b/lib/puppet/module_tool/applications/upgrader.rb index d1c4a9532..bb5ac82bb 100644 --- a/lib/puppet/module_tool/applications/upgrader.rb +++ b/lib/puppet/module_tool/applications/upgrader.rb @@ -1,279 +1,277 @@ require 'pathname' require 'puppet/forge' require 'puppet/module_tool' require 'puppet/module_tool/shared_behaviors' require 'puppet/module_tool/install_directory' require 'puppet/module_tool/installed_modules' module Puppet::ModuleTool module Applications class Upgrader < Application include Puppet::ModuleTool::Errors def initialize(name, options) super(options) @action = :upgrade @environment = options[:environment_instance] @name = name @ignore_changes = forced? || options[:ignore_changes] @ignore_dependencies = forced? || options[:ignore_dependencies] Semantic::Dependency.add_source(installed_modules_source) Semantic::Dependency.add_source(module_repository) end def run name = @name.tr('/', '-') version = options[:version] || '>= 0.0.0' results = { :action => :upgrade, :requested_version => options[:version] || :latest, } begin all_modules = @environment.modules_by_path.values.flatten matching_modules = all_modules.select do |x| x.forge_name && x.forge_name.tr('/', '-') == name end if matching_modules.empty? raise NotInstalledError, results.merge(:module_name => name) elsif matching_modules.length > 1 raise MultipleInstalledError, results.merge(:module_name => name, :installed_modules => matching_modules) end installed_release = installed_modules[name] # `priority` is an attribute of a `Semantic::Dependency::Source`, # which is delegated through `ModuleRelease` instances for the sake of # comparison (sorting). By default, the `InstalledModules` source has # a priority of 10 (making it the most preferable source, so that # already installed versions of modules are selected in preference to # modules from e.g. the Forge). Since we are specifically looking to # upgrade this module, we don't want the installed version of this # module to be chosen in preference to those with higher versions. # # This implementation is suboptimal, and since we can expect this sort # of behavior to be reasonably common in Semantic, we should probably # see about implementing a `ModuleRelease#override_priority` method # (or something similar). def installed_release.priority 0 end mod = installed_release.mod results[:installed_version] = Semantic::Version.parse(mod.version) dir = Pathname.new(mod.modulepath) vstring = mod.version ? "v#{mod.version}" : '???' Puppet.notice "Found '#{name}' (#{colorize(:cyan, vstring)}) in #{dir} ..." unless @ignore_changes changes = Checksummer.run(mod.path) rescue [] if mod.has_metadata? && !changes.empty? raise LocalChangesError, :action => :upgrade, :module_name => name, :requested_version => results[:requested_version], :installed_version => mod.version end end Puppet::Forge::Cache.clean # Ensure that there is at least one candidate release available # for the target package. available_versions = module_repository.fetch(name) if available_versions.empty? raise NoCandidateReleasesError, results.merge(:module_name => name, :source => module_repository.host) elsif results[:requested_version] != :latest requested = Semantic::VersionRange.parse(results[:requested_version]) unless available_versions.any? {|m| requested.include? m.version} raise NoCandidateReleasesError, results.merge(:module_name => name, :source => module_repository.host) end end Puppet.notice "Downloading from #{module_repository.host} ..." if @ignore_dependencies graph = build_single_module_graph(name, version) else graph = build_dependency_graph(name, version) end unless forced? add_module_name_constraints_to_graph(graph) end installed_modules.each do |installed_module, release| installed_module = installed_module.tr('/', '-') next if installed_module == name version = release.version unless forced? # Since upgrading already installed modules can be troublesome, # we'll place constraints on the graph for each installed # module, locking it to upgrades within the same major version. - ">=#{version} #{version.major}.x".tap do |range| - graph.add_constraint('installed', installed_module, range) do |node| - Semantic::VersionRange.parse(range).include? node.version - end + installed_range = ">=#{version} #{version.major}.x" + graph.add_constraint('installed', installed_module, installed_range) do |node| + Semantic::VersionRange.parse(installed_range).include? node.version end release.mod.dependencies.each do |dep| dep_name = dep['name'].tr('/', '-') - dep['version_requirement'].tap do |range| - graph.add_constraint("#{installed_module} constraint", dep_name, range) do |node| - Semantic::VersionRange.parse(range).include? node.version - end + range = dep['version_requirement'] + graph.add_constraint("#{installed_module} constraint", dep_name, range) do |node| + Semantic::VersionRange.parse(range).include? node.version end end end end begin Puppet.info "Resolving dependencies ..." releases = Semantic::Dependency.resolve(graph) rescue Semantic::Dependency::UnsatisfiableGraph raise NoVersionsSatisfyError, results.merge(:requested_name => name) end releases.each do |rel| if mod = installed_modules_source.by_name[rel.name.split('-').last] next if mod.has_metadata? && mod.forge_name.tr('/', '-') == rel.name if rel.name != name dependency = { :name => rel.name, :version => rel.version } end raise InstallConflictError, :requested_module => name, :requested_version => options[:version] || 'latest', :dependency => dependency, :directory => mod.path, :metadata => mod.metadata end end child = releases.find { |x| x.name == name } unless forced? if child.version == results[:installed_version] versions = graph.dependencies[name].map { |r| r.version } newer_versions = versions.select { |v| v > results[:installed_version] } raise VersionAlreadyInstalledError, :module_name => name, :requested_version => results[:requested_version], :installed_version => results[:installed_version], :newer_versions => newer_versions, :possible_culprits => installed_modules_source.fetched.reject { |x| x == name } elsif child.version < results[:installed_version] raise DowngradingUnsupportedError, :module_name => name, :requested_version => results[:requested_version], :installed_version => results[:installed_version] end end Puppet.info "Preparing to upgrade ..." releases.each { |release| release.prepare } Puppet.notice 'Upgrading -- do not interrupt ...' releases.each do |release| if installed = installed_modules[release.name] release.install(Pathname.new(installed.mod.modulepath)) else release.install(dir) end end results[:result] = :success results[:base_dir] = releases.first.install_dir results[:affected_modules] = releases results[:graph] = [ build_install_graph(releases.first, releases) ] rescue VersionAlreadyInstalledError => e results[:result] = (e.newer_versions.empty? ? :noop : :failure) results[:error] = { :oneline => e.message, :multiline => e.multiline } rescue => e results[:error] = { :oneline => e.message, :multiline => e.respond_to?(:multiline) ? e.multiline : [e.to_s, e.backtrace].join("\n") } ensure results[:result] ||= :failure end results end private def module_repository @repo ||= Puppet::Forge.new end def installed_modules_source @installed ||= Puppet::ModuleTool::InstalledModules.new(@environment) end def installed_modules installed_modules_source.modules end def build_single_module_graph(name, version) range = Semantic::VersionRange.parse(version) graph = Semantic::Dependency::Graph.new(name => range) releases = Semantic::Dependency.fetch_releases(name) releases.each { |release| release.dependencies.clear } graph << releases end def build_dependency_graph(name, version) Semantic::Dependency.query(name => version) end def build_install_graph(release, installed, graphed = []) previous = installed_modules[release.name] previous = previous.version if previous action = :upgrade unless previous && previous != release.version action = :install end graphed << release dependencies = release.dependencies.values.map do |deps| dep = (deps & installed).first if dep == installed_modules[dep.name] next end if dep && !graphed.include?(dep) build_install_graph(dep, installed, graphed) end end.compact return { :release => release, :name => release.name, :path => release.install_dir, :dependencies => dependencies.compact, :version => release.version, :previous_version => previous, :action => action, } end include Puppet::ModuleTool::Shared end end end