diff --git a/acceptance/tests/modules/upgrade/with_local_changes.rb b/acceptance/tests/modules/upgrade/with_local_changes.rb index 76531582e..4163665a0 100644 --- a/acceptance/tests/modules/upgrade/with_local_changes.rb +++ b/acceptance/tests/modules/upgrade/with_local_changes.rb @@ -1,54 +1,54 @@ test_name "puppet module upgrade (with local changes)" step 'Setup' stub_forge_on(master) on master, "mkdir -p #{master['distmoduledir']}" teardown do on master, "rm -rf #{master['distmoduledir']}/java" on master, "rm -rf #{master['distmoduledir']}/stdlub" end on master, puppet("module install pmtacceptance-java --version 1.6.0") on master, puppet("module list --modulepath #{master['distmoduledir']}") do assert_equal <<-OUTPUT, stdout #{master['distmoduledir']} ├── pmtacceptance-java (\e[0;36mv1.6.0\e[0m) └── pmtacceptance-stdlub (\e[0;36mv1.0.0\e[0m) OUTPUT end apply_manifest_on master, <<-PP file { '#{master['distmoduledir']}/java/README': content => "I CHANGE MY READMES"; '#{master['distmoduledir']}/java/NEWFILE': content => "I don't exist.'"; } PP step "Try to upgrade a module with local changes" on master, puppet("module upgrade pmtacceptance-java"), :acceptable_exit_codes => [1] do pattern = Regexp.new([ %Q{.*Notice: Preparing to upgrade 'pmtacceptance-java' ....*}, %Q{.*Notice: Found 'pmtacceptance-java' \\(.*v1.6.0.*\\) in #{master['distmoduledir']} ....*}, %Q{.*Error: Could not upgrade module 'pmtacceptance-java' \\(v1.6.0 -> latest\\)}, %Q{ Installed module has had changes made locally}, - %Q{ Use `puppet module upgrade --force` to upgrade this module anyway.*}, + %Q{ Use `puppet module upgrade --ignore-changes` to upgrade this module anyway.*}, ].join("\n"), Regexp::MULTILINE) assert_match(pattern, result.output) end on master, %{[[ "$(cat #{master['distmoduledir']}/java/README)" == "I CHANGE MY READMES" ]]} on master, "[ -f #{master['distmoduledir']}/java/NEWFILE ]" step "Upgrade a module with local changes with --force" on master, puppet("module upgrade pmtacceptance-java --force") do assert_equal <<-OUTPUT, stdout \e[mNotice: Preparing to upgrade 'pmtacceptance-java' ...\e[0m \e[mNotice: Found 'pmtacceptance-java' (\e[0;36mv1.6.0\e[m) in #{master['distmoduledir']} ...\e[0m \e[mNotice: Downloading from https://forgeapi.puppetlabs.com ...\e[0m \e[mNotice: Upgrading -- do not interrupt ...\e[0m #{master['distmoduledir']} └── pmtacceptance-java (\e[0;36mv1.6.0 -> v1.7.1\e[0m) OUTPUT end on master, %{[[ "$(cat #{master['distmoduledir']}/java/README)" != "I CHANGE MY READMES" ]]} on master, "[ ! -f #{master['distmoduledir']}/java/NEWFILE ]" diff --git a/acceptance/tests/store_configs/enc_provides_node_when_storeconfigs_enabled.rb b/acceptance/tests/store_configs/enc_provides_node_when_storeconfigs_enabled.rb index 79ec5f03c..c50cda655 100644 --- a/acceptance/tests/store_configs/enc_provides_node_when_storeconfigs_enabled.rb +++ b/acceptance/tests/store_configs/enc_provides_node_when_storeconfigs_enabled.rb @@ -1,125 +1,121 @@ test_name "ENC node information is used when store configs enabled (#16698)" -confine :except, :platform => 'solaris' -confine :except, :platform => 'windows' -confine :except, :platform => 'el-6' -confine :except, :platform => 'el-7' +confine :to, :platform => ['debian', 'ubuntu'] confine :except, :platform => 'lucid' -confine :except, :platform => 'sles-11' testdir = master.tmpdir('use_enc') create_remote_file master, "#{testdir}/enc.rb", < [], 'parameters' => { 'data' => 'data from enc' }, }.to_yaml) END on master, "chmod 755 #{testdir}/enc.rb" create_remote_file(master, "#{testdir}/site.pp", 'notify { $data: }') on master, "chown -R #{master['user']}:#{master['group']} #{testdir}" on master, "chmod -R g+rwX #{testdir}" create_remote_file master, "#{testdir}/setup.pp", < $lsbmajdistrelease ? { 5 => '2.2.3', default => '3.2.16', }, default => '3.2.16', } # Trusty doesn't have a rubygems package anymore # Not sure which other Debian's might follow suit so # restricting this narrowly for now # if $lsbdistid == "Ubuntu" and $lsbdistrelease == "14.04" { package { activerecord: ensure => $active_record_version, provider => 'gem', } } else { package { rubygems: ensure => present; activerecord: ensure => $active_record_version, provider => 'gem', require => Package[rubygems]; } } if $osfamily == "Debian" { package { # This is the deb sqlite3 package sqlite3: ensure => present; libsqlite3-dev: ensure => present, require => Package[sqlite3]; } } elsif $osfamily == "RedHat" { $sqlite_gem_pkg_name = $operatingsystem ? { "Fedora" => "rubygem-sqlite3", default => "rubygem-sqlite3-ruby" } package { sqlite: ensure => present; $sqlite_gem_pkg_name: ensure => present, require => Package[sqlite] } } else { fail "Unknown OS $osfamily" } END # This is a brute force hack around PUP-1073 because the deb for the core # sqlite3 package and the rubygem for the sqlite3 driver are both named # 'sqlite3'. So we just run a second puppet apply. create_remote_file master, "#{testdir}/setup_sqlite_gem.pp", < 'sqlite3', ensure => present, provider => 'gem', } } END on master, puppet_apply("#{testdir}/setup.pp") on master, puppet_apply("#{testdir}/setup_sqlite_gem.pp") master_opts = { 'master' => { 'node_terminus' => 'exec', 'external_nodes' => "#{testdir}/enc.rb", 'storeconfigs' => true, 'dbadapter' => 'sqlite3', 'dblocation' => "#{testdir}/store_configs.sqlite3", 'manifest' => "#{testdir}/site.pp" } } with_puppet_running_on master, master_opts, testdir do agents.each do |agent| run_agent_on(agent, "--no-daemonize --onetime --server #{master} --verbose") assert_match(/data from enc/, stdout) end end diff --git a/lib/puppet/face/module/uninstall.rb b/lib/puppet/face/module/uninstall.rb index 60db2c2b7..2a5e675b2 100644 --- a/lib/puppet/face/module/uninstall.rb +++ b/lib/puppet/face/module/uninstall.rb @@ -1,71 +1,78 @@ Puppet::Face.define(:module, '1.0.0') do action(:uninstall) do summary "Uninstall a puppet module." description <<-EOT Uninstalls a puppet module from the modulepath (or a specific target directory). EOT returns "Hash of module objects representing uninstalled modules and related errors." examples <<-'EOT' Uninstall a module: $ puppet module uninstall puppetlabs-ssh Removed /etc/puppet/modules/ssh (v1.0.0) Uninstall a module from a specific directory: $ puppet module uninstall puppetlabs-ssh --modulepath /usr/share/puppet/modules Removed /usr/share/puppet/modules/ssh (v1.0.0) Uninstall a module from a specific environment: $ puppet module uninstall puppetlabs-ssh --environment development Removed /etc/puppet/environments/development/modules/ssh (v1.0.0) Uninstall a specific version of a module: $ puppet module uninstall puppetlabs-ssh --version 2.0.0 Removed /etc/puppet/modules/ssh (v2.0.0) EOT arguments "" option "--force", "-f" do summary "Force uninstall of an installed module." description <<-EOT Force the uninstall of an installed module even if there are local changes or the possibility of causing broken dependencies. EOT end + option "--ignore-changes", "-c" do + summary "Ignore any local changes made. (Implied by --force.)" + description <<-EOT + Uninstall an installed module even if there are local changes to it. (Implied by --force.) + EOT + end + option "--version=" do summary "The version of the module to uninstall" description <<-EOT The version of the module to uninstall. When using this option, a module matching the specified version must be installed or else an error is raised. EOT end when_invoked do |name, options| name = name.gsub('/', '-') Puppet::ModuleTool.set_option_defaults options Puppet.notice "Preparing to uninstall '#{name}'" << (options[:version] ? " (#{colorize(:cyan, options[:version].sub(/^(?=\d)/, 'v'))})" : '') << " ..." Puppet::ModuleTool::Applications::Uninstaller.run(name, options) end when_rendering :console do |return_value| if return_value[:result] == :failure Puppet.err(return_value[:error][:multiline]) exit 1 else mod = return_value[:affected_modules].first "Removed '#{return_value[:module_name]}'" << (mod.version ? " (#{colorize(:cyan, mod.version.to_s.sub(/^(?=\d)/, 'v'))})" : '') << " from #{mod.modulepath}" end end end end diff --git a/lib/puppet/face/module/upgrade.rb b/lib/puppet/face/module/upgrade.rb index 2cb197702..f671887e1 100644 --- a/lib/puppet/face/module/upgrade.rb +++ b/lib/puppet/face/module/upgrade.rb @@ -1,79 +1,86 @@ # encoding: UTF-8 Puppet::Face.define(:module, '1.0.0') do action(:upgrade) do summary "Upgrade a puppet module." description <<-EOT Upgrades a puppet module. EOT returns "Hash" examples <<-EOT upgrade an installed module to the latest version $ puppet module upgrade puppetlabs-apache /etc/puppet/modules └── puppetlabs-apache (v1.0.0 -> v2.4.0) upgrade an installed module to a specific version $ puppet module upgrade puppetlabs-apache --version 2.1.0 /etc/puppet/modules └── puppetlabs-apache (v1.0.0 -> v2.1.0) upgrade an installed module for a specific environment $ puppet module upgrade puppetlabs-apache --environment test /usr/share/puppet/environments/test/modules └── puppetlabs-apache (v1.0.0 -> v2.4.0) EOT arguments "" option "--force", "-f" do summary "Force upgrade of an installed module. (Implies --ignore-dependencies.)" description <<-EOT Force the upgrade of an installed module even if there are local changes or the possibility of causing broken dependencies. Implies --ignore-dependencies. EOT end option "--ignore-dependencies" do summary "Do not attempt to install dependencies. (Implied by --force.)" description <<-EOT Do not attempt to install dependencies. Implied by --force. EOT end + option "--ignore-changes", "-c" do + summary "Ignore and overwrite any local changes made. (Implied by --force.)" + description <<-EOT + Upgrade an installed module even if there are local changes to it. (Implied by --force.) + EOT + end + option "--version=" do summary "The version of the module to upgrade to." description <<-EOT The version of the module to upgrade to. EOT end when_invoked do |name, options| name = name.gsub('/', '-') Puppet.notice "Preparing to upgrade '#{name}' ..." Puppet::ModuleTool.set_option_defaults options Puppet::ModuleTool::Applications::Upgrader.new(name, options).run end when_rendering :console do |return_value| if return_value[:result] == :noop Puppet.notice return_value[:error][:multiline] exit 0 elsif return_value[:result] == :failure Puppet.err(return_value[:error][:multiline]) exit 1 else tree = Puppet::ModuleTool.build_tree(return_value[:graph], return_value[:base_dir]) "#{return_value[:base_dir]}\n" + Puppet::ModuleTool.format_tree(tree) end end end end diff --git a/lib/puppet/module_tool/applications/uninstaller.rb b/lib/puppet/module_tool/applications/uninstaller.rb index 01465cb65..46e5391be 100644 --- a/lib/puppet/module_tool/applications/uninstaller.rb +++ b/lib/puppet/module_tool/applications/uninstaller.rb @@ -1,116 +1,117 @@ module Puppet::ModuleTool module Applications class Uninstaller < Application include Puppet::ModuleTool::Errors def initialize(name, options) @name = name @options = options @errors = Hash.new {|h, k| h[k] = {}} @unfiltered = [] @installed = [] @suggestions = [] @environment = options[:environment_instance] + @ignore_changes = options[:force] || options[:ignore_changes] end def run results = { :module_name => @name, :requested_version => @version, } begin find_installed_module validate_module FileUtils.rm_rf(@installed.first.path, :secure => true) results[:affected_modules] = @installed results[:result] = :success rescue ModuleToolError => err results[:error] = { :oneline => err.message, :multiline => err.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 find_installed_module @environment.modules_by_path.values.flatten.each do |mod| mod_name = (mod.forge_name || mod.name).gsub('/', '-') if mod_name == @name @unfiltered << { :name => mod_name, :version => mod.version, :path => mod.modulepath, } if @options[:version] && mod.version next unless SemVer[@options[:version]].include?(SemVer.new(mod.version)) end @installed << mod elsif mod_name =~ /#{@name}/ @suggestions << mod_name end end if @installed.length > 1 raise MultipleInstalledError, :action => :uninstall, :module_name => @name, :installed_modules => @installed.sort_by { |mod| @environment.modulepath.index(mod.modulepath) } elsif @installed.empty? if @unfiltered.empty? raise NotInstalledError, :action => :uninstall, :suggestions => @suggestions, :module_name => @name else raise NoVersionMatchesError, :installed_modules => @unfiltered.sort_by { |mod| @environment.modulepath.index(mod[:path]) }, :version_range => @options[:version], :module_name => @name end end end def validate_module mod = @installed.first - if !@options[:force] && mod.has_metadata? + unless @ignore_changes changes = begin Puppet::ModuleTool::Applications::Checksummer.run(mod.path) rescue ArgumentError [] end - if !changes.empty? + if mod.has_metadata? && !changes.empty? raise LocalChangesError, :action => :uninstall, :module_name => (mod.forge_name || mod.name).gsub('/', '-'), :requested_version => @options[:version], :installed_version => mod.version end end if !@options[:force] && !mod.required_by.empty? raise ModuleIsRequiredError, :module_name => (mod.forge_name || mod.name).gsub('/', '-'), :required_by => mod.required_by, :requested_version => @options[:version], :installed_version => mod.version end end end end end diff --git a/lib/puppet/module_tool/applications/upgrader.rb b/lib/puppet/module_tool/applications/upgrader.rb index 2032bc09f..420f04a65 100644 --- a/lib/puppet/module_tool/applications/upgrader.rb +++ b/lib/puppet/module_tool/applications/upgrader.rb @@ -1,271 +1,272 @@ 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 mod = 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 mod.priority 0 end mod = mod.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 forced? + 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 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 |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 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 end end end end # Ensure that there is at least one candidate release available # for the target package. if graph.dependencies[name].empty? if results[:requested_version] == :latest || !Semantic::VersionRange.parse(results[:requested_version]).include?(results[:installed_version]) raise NoCandidateReleasesError, results.merge(:module_name => name, :source => module_repository.host) end elsif graph.dependencies[name] == SortedSet.new([installed_modules[name]]) raise VersionAlreadyInstalledError, results.merge(:module_name => name, :newer_versions => []) 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 } 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 diff --git a/lib/puppet/module_tool/errors/shared.rb b/lib/puppet/module_tool/errors/shared.rb index c49342834..a1e02827c 100644 --- a/lib/puppet/module_tool/errors/shared.rb +++ b/lib/puppet/module_tool/errors/shared.rb @@ -1,188 +1,188 @@ module Puppet::ModuleTool::Errors class NoVersionsSatisfyError < ModuleToolError def initialize(options) @requested_name = options[:requested_name] @requested_version = options[:requested_version] @installed_version = options[:installed_version] @conditions = options[:conditions] @action = options[:action] super "Could not #{@action} '#{@requested_name}' (#{vstring}); no version satisfies all dependencies" end def multiline message = [] message << "Could not #{@action} module '#{@requested_name}' (#{vstring})" message << " No version of '#{@requested_name}' can satisfy all dependencies" message << " Use `puppet module #{@action} --ignore-dependencies` to #{@action} only this module" message.join("\n") end end class NoCandidateReleasesError < ModuleToolError def initialize(options) @module_name = options[:module_name] @requested_version = options[:requested_version] @installed_version = options[:installed_version] @source = options[:source] @action = options[:action] if @requested_version == :latest super "Could not #{@action} '#{@module_name}'; no releases are available from #{@source}" else super "Could not #{@action} '#{@module_name}'; no releases matching '#{@requested_version}' are available from #{@source}" end end def multiline message = [] message << "Could not #{@action} '#{@module_name}' (#{vstring})" if @requested_version == :latest message << " No releases are available from #{@source}" message << " Does '#{@module_name}' have at least one published release?" else message << " No releases matching '#{@requested_version}' are available from #{@source}" end message.join("\n") end end class InstallConflictError < ModuleToolError def initialize(options) @requested_module = options[:requested_module] @requested_version = v(options[:requested_version]) @dependency = options[:dependency] @directory = options[:directory] @metadata = options[:metadata] super "'#{@requested_module}' (#{@requested_version}) requested; installation conflict" end def multiline message = [] message << "Could not install module '#{@requested_module}' (#{@requested_version})" if @dependency message << " Dependency '#{@dependency[:name]}' (#{v(@dependency[:version])}) would overwrite #{@directory}" else message << " Installation would overwrite #{@directory}" end if @metadata message << " Currently, '#{@metadata["name"]}' (#{v(@metadata["version"])}) is installed to that directory" end if @dependency message << " Use `puppet module install --ignore-dependencies` to install only this module" else message << " Use `puppet module install --force` to install this module anyway" end message.join("\n") end end class InvalidDependencyCycleError < ModuleToolError def initialize(options) @module_name = options[:module_name] @requested_module = options[:requested_module] @requested_version = options[:requested_version] @conditions = options[:conditions] @source = options[:source][1..-1] super "'#{@requested_module}' (#{v(@requested_version)}) requested; Invalid dependency cycle" end def multiline trace = [] trace << "You specified '#{@source.first[:name]}' (#{v(@requested_version)})" trace += @source[1..-1].map { |m| "which depends on '#{m[:name]}' (#{v(m[:version])})" } message = [] message << "Could not install module '#{@requested_module}' (#{v(@requested_version)})" message << " No version of '#{@module_name}' will satisfy dependencies" message << trace.map { |s| " #{s}" }.join(",\n") message << " Use `puppet module install --force` to install this module anyway" message.join("\n") end end class NotInstalledError < ModuleToolError def initialize(options) @module_name = options[:module_name] @suggestions = options[:suggestions] || [] @action = options[:action] super "Could not #{@action} '#{@module_name}'; module is not installed" end def multiline message = [] message << "Could not #{@action} module '#{@module_name}'" message << " Module '#{@module_name}' is not installed" message += @suggestions.map do |suggestion| " You may have meant `puppet module #{@action} #{suggestion}`" end message << " Use `puppet module install` to install this module" if @action == :upgrade message.join("\n") end end class MultipleInstalledError < ModuleToolError def initialize(options) @module_name = options[:module_name] @modules = options[:installed_modules] @action = options[:action] super "Could not #{@action} '#{@module_name}'; module appears in multiple places in the module path" end def multiline message = [] message << "Could not #{@action} module '#{@module_name}'" message << " Module '#{@module_name}' appears multiple places in the module path" message += @modules.map do |mod| " '#{@module_name}' (#{v(mod.version)}) was found in #{mod.modulepath}" end message << " Use the `--modulepath` option to limit the search to specific directories" message.join("\n") end end class LocalChangesError < ModuleToolError def initialize(options) @module_name = options[:module_name] @requested_version = options[:requested_version] @installed_version = options[:installed_version] @action = options[:action] super "Could not #{@action} '#{@module_name}'; module has had changes made locally" end def multiline message = [] message << "Could not #{@action} module '#{@module_name}' (#{vstring})" message << " Installed module has had changes made locally" - message << " Use `puppet module #{@action} --force` to #{@action} this module anyway" + message << " Use `puppet module #{@action} --ignore-changes` to #{@action} this module anyway" message.join("\n") end end class InvalidModuleError < ModuleToolError def initialize(name, options) @name = name @action = options[:action] @error = options[:error] super "Could not #{@action} '#{@name}'; #{@error.message}" end def multiline message = [] message << "Could not #{@action} module '#{@name}'" message << " Failure trying to parse metadata" message << " Original message was: #{@error.message}" message.join("\n") end end end diff --git a/spec/unit/module_tool/applications/uninstaller_spec.rb b/spec/unit/module_tool/applications/uninstaller_spec.rb index 2a8562ab9..66e71b638 100644 --- a/spec/unit/module_tool/applications/uninstaller_spec.rb +++ b/spec/unit/module_tool/applications/uninstaller_spec.rb @@ -1,143 +1,165 @@ require 'spec_helper' require 'puppet/module_tool' require 'tmpdir' require 'puppet_spec/module_tool/shared_functions' require 'puppet_spec/module_tool/stub_source' describe Puppet::ModuleTool::Applications::Uninstaller do include PuppetSpec::ModuleTool::SharedFunctions include PuppetSpec::Files before do FileUtils.mkdir_p(primary_dir) FileUtils.mkdir_p(secondary_dir) end let(:environment) do Puppet.lookup(:current_environment).override_with( :vardir => vardir, :modulepath => [ primary_dir, secondary_dir ] ) end let(:vardir) { tmpdir('uninstaller') } let(:primary_dir) { File.join(vardir, "primary") } let(:secondary_dir) { File.join(vardir, "secondary") } let(:remote_source) { PuppetSpec::ModuleTool::StubSource.new } let(:module) { 'module-not_installed' } let(:application) do opts = options Puppet::ModuleTool.set_option_defaults(opts) Puppet::ModuleTool::Applications::Uninstaller.new(self.module, opts) end def options { :environment => environment } end subject { application.run } context "when the module is not installed" do it "should fail" do subject.should include :result => :failure end end context "when the module is installed" do let(:module) { 'pmtacceptance-stdlib' } before { preinstall('pmtacceptance-stdlib', '1.0.0') } before { preinstall('pmtacceptance-apache', '0.0.4') } it "should uninstall the module" do subject[:affected_modules].first.forge_name.should == "pmtacceptance/stdlib" end it "should only uninstall the requested module" do subject[:affected_modules].length == 1 end context 'in two modulepaths' do before { preinstall('pmtacceptance-stdlib', '2.0.0', :into => secondary_dir) } it "should fail if a module exists twice in the modpath" do subject.should include :result => :failure end end context "when options[:version] is specified" do def options super.merge(:version => '1.0.0') end it "should uninstall the module if the version matches" do subject[:affected_modules].length.should == 1 subject[:affected_modules].first.version.should == "1.0.0" end context 'but not matched' do def options super.merge(:version => '2.0.0') end it "should not uninstall the module if the version does not match" do subject.should include :result => :failure end end end context "when the module metadata is missing" do before { File.unlink(File.join(primary_dir, 'stdlib', 'metadata.json')) } it "should not uninstall the module" do application.run[:result].should == :failure end end context "when the module has local changes" do before do mark_changed(File.join(primary_dir, 'stdlib')) end it "should not uninstall the module" do subject.should include :result => :failure end end context "when uninstalling the module will cause broken dependencies" do before { preinstall('pmtacceptance-apache', '0.10.0') } it "should not uninstall the module" do subject.should include :result => :failure end end + context 'with --ignore-changes' do + def options + super.merge(:ignore_changes => true) + end + + context 'with local changes' do + before do + mark_changed(File.join(primary_dir, 'stdlib')) + end + + it 'overwrites the installed module with the greatest version matching that range' do + subject.should include :result => :success + end + end + + context 'without local changes' do + it 'overwrites the installed module with the greatest version matching that range' do + subject.should include :result => :success + end + end + end + context "when using the --force flag" do def options super.merge(:force => true) end context "with local changes" do before do mark_changed(File.join(primary_dir, 'stdlib')) end it "should ignore local changes" do subject[:affected_modules].length.should == 1 subject[:affected_modules].first.forge_name.should == "pmtacceptance/stdlib" end end context "while depended upon" do before { preinstall('pmtacceptance-apache', '0.10.0') } it "should ignore broken dependencies" do subject[:affected_modules].length.should == 1 subject[:affected_modules].first.forge_name.should == "pmtacceptance/stdlib" end end end end end diff --git a/spec/unit/module_tool/applications/upgrader_spec.rb b/spec/unit/module_tool/applications/upgrader_spec.rb index 4c00aa535..382e45a75 100644 --- a/spec/unit/module_tool/applications/upgrader_spec.rb +++ b/spec/unit/module_tool/applications/upgrader_spec.rb @@ -1,313 +1,324 @@ require 'spec_helper' require 'puppet/module_tool/applications' require 'puppet_spec/module_tool/shared_functions' require 'puppet_spec/module_tool/stub_source' require 'semver' describe Puppet::ModuleTool::Applications::Upgrader do include PuppetSpec::ModuleTool::SharedFunctions include PuppetSpec::Files before do FileUtils.mkdir_p(primary_dir) FileUtils.mkdir_p(secondary_dir) end let(:vardir) { tmpdir('upgrader') } let(:primary_dir) { File.join(vardir, "primary") } let(:secondary_dir) { File.join(vardir, "secondary") } let(:remote_source) { PuppetSpec::ModuleTool::StubSource.new } let(:environment) do Puppet.lookup(:current_environment).override_with( :vardir => vardir, :modulepath => [ primary_dir, secondary_dir ] ) end before do Semantic::Dependency.clear_sources installer = Puppet::ModuleTool::Applications::Upgrader.any_instance installer.stubs(:module_repository).returns(remote_source) end def upgrader(name, options = {}) Puppet::ModuleTool.set_option_defaults(options) Puppet::ModuleTool::Applications::Upgrader.new(name, options) end describe '#run' do let(:module) { 'pmtacceptance-stdlib' } def options { :environment => environment } end let(:application) { upgrader(self.module, options) } subject { application.run } it 'fails if the module is not already installed' do subject.should include :result => :failure subject[:error].should include :oneline => "Could not upgrade '#{self.module}'; module is not installed" end context 'for an installed module' do context 'with only one version' do before { preinstall('puppetlabs-oneversion', '0.0.1') } let(:module) { 'puppetlabs-oneversion' } it 'declines to upgrade' do subject.should include :result => :noop subject[:error][:multiline].should =~ /already the latest version/ end end context 'without dependencies' do before { preinstall('pmtacceptance-stdlib', '1.0.0') } context 'without options' do it 'properly upgrades the module to the greatest version' do subject.should include :result => :success graph_should_include 'pmtacceptance-stdlib', v('1.0.0') => v('4.1.0') end end context 'with version range' do def options super.merge(:version => '3.x') end context 'not matching the installed version' do it 'properly upgrades the module to the greatest version within that range' do subject.should include :result => :success graph_should_include 'pmtacceptance-stdlib', v('1.0.0') => v('3.2.0') end end context 'matching the installed version' do context 'with more recent version' do before { preinstall('pmtacceptance-stdlib', '3.0.0')} it 'properly upgrades the module to the greatest version within that range' do subject.should include :result => :success graph_should_include 'pmtacceptance-stdlib', v('3.0.0') => v('3.2.0') end end context 'without more recent version' do before { preinstall('pmtacceptance-stdlib', '3.2.0')} context 'without options' do it 'declines to upgrade' do subject.should include :result => :noop subject[:error][:multiline].should =~ /already the latest version/ end end context 'with --force' do def options super.merge(:force => true) end it 'overwrites the installed module with the greatest version matching that range' do subject.should include :result => :success graph_should_include 'pmtacceptance-stdlib', v('3.2.0') => v('3.2.0') end end end end end end context 'that is depended upon' do # pmtacceptance-keystone depends on pmtacceptance-mysql >=0.6.1 <1.0.0 before { preinstall('pmtacceptance-keystone', '2.1.0') } before { preinstall('pmtacceptance-mysql', '0.9.0') } let(:module) { 'pmtacceptance-mysql' } context 'and out of date' do before { preinstall('pmtacceptance-mysql', '0.8.0') } it 'properly upgrades to the greatest version matching the dependency' do subject.should include :result => :success graph_should_include 'pmtacceptance-mysql', v('0.8.0') => v('0.9.0') end end context 'and up to date' do it 'declines to upgrade' do subject.should include :result => :failure end end context 'when specifying a violating version range' do def options super.merge(:version => '2.1.0') end it 'fails to upgrade the module' do # TODO: More helpful error message? subject.should include :result => :failure subject[:error].should include :oneline => "Could not upgrade '#{self.module}' (v0.9.0 -> v2.1.0); no version satisfies all dependencies" end context 'using --force' do def options super.merge(:force => true) end it 'overwrites the installed module with the specified version' do subject.should include :result => :success graph_should_include 'pmtacceptance-mysql', v('0.9.0') => v('2.1.0') end end end end context 'with local changes' do before { preinstall('pmtacceptance-stdlib', '1.0.0') } before do release = application.send(:installed_modules)['pmtacceptance-stdlib'] mark_changed(release.mod.path) end it 'fails to upgrade' do subject.should include :result => :failure subject[:error].should include :oneline => "Could not upgrade '#{self.module}'; module has had changes made locally" end + + context 'with --ignore-changes' do + def options + super.merge(:ignore_changes => true) + end + + it 'overwrites the installed module with the greatest version matching that range' do + subject.should include :result => :success + graph_should_include 'pmtacceptance-stdlib', v('1.0.0') => v('4.1.0') + end + end end context 'with dependencies' do context 'that are unsatisfied' do def options super.merge(:version => '0.1.1') end before { preinstall('pmtacceptance-apache', '0.0.3') } let(:module) { 'pmtacceptance-apache' } it 'upgrades the module and installs the missing dependencies' do subject.should include :result => :success graph_should_include 'pmtacceptance-apache', v('0.0.3') => v('0.1.1') graph_should_include 'pmtacceptance-stdlib', nil => v('4.1.0'), :action => :install end end context 'with older major versions' do # pmtacceptance-apache 0.0.4 has no dependency on pmtacceptance-stdlib # the next available version (0.1.1) and all subsequent versions depend on pmtacceptance-stdlib >= 2.2.1 before { preinstall('pmtacceptance-apache', '0.0.3') } before { preinstall('pmtacceptance-stdlib', '1.0.0') } let(:module) { 'pmtacceptance-apache' } it 'refuses to upgrade the installed dependency to a new major version, but upgrades the module to the greatest compatible version' do subject.should include :result => :success graph_should_include 'pmtacceptance-apache', v('0.0.3') => v('0.0.4') end context 'using --ignore_dependencies' do def options super.merge(:ignore_dependencies => true) end it 'upgrades the module to the greatest available version' do subject.should include :result => :success graph_should_include 'pmtacceptance-apache', v('0.0.3') => v('0.10.0') end end end context 'with satisfying major versions' do before { preinstall('pmtacceptance-apache', '0.0.3') } before { preinstall('pmtacceptance-stdlib', '2.0.0') } let(:module) { 'pmtacceptance-apache' } it 'upgrades the module and its dependencies to their greatest compatible versions' do subject.should include :result => :success graph_should_include 'pmtacceptance-apache', v('0.0.3') => v('0.10.0') graph_should_include 'pmtacceptance-stdlib', v('2.0.0') => v('2.6.0') end end context 'with satisfying versions' do before { preinstall('pmtacceptance-apache', '0.0.3') } before { preinstall('pmtacceptance-stdlib', '2.4.0') } let(:module) { 'pmtacceptance-apache' } it 'upgrades the module to the greatest available version' do subject.should include :result => :success graph_should_include 'pmtacceptance-apache', v('0.0.3') => v('0.10.0') graph_should_include 'pmtacceptance-stdlib', nil end end context 'with current versions' do before { preinstall('pmtacceptance-apache', '0.0.3') } before { preinstall('pmtacceptance-stdlib', '2.6.0') } let(:module) { 'pmtacceptance-apache' } it 'upgrades the module to the greatest available version' do subject.should include :result => :success graph_should_include 'pmtacceptance-apache', v('0.0.3') => v('0.10.0') graph_should_include 'pmtacceptance-stdlib', nil end end context 'with shared dependencies' do # bacula 0.0.3 depends on stdlib >= 2.2.0 and pmtacceptance/mysql >= 1.0.0 # bacula 0.0.2 depends on stdlib >= 2.2.0 and pmtacceptance/mysql >= 0.0.1 # bacula 0.0.1 depends on stdlib >= 2.2.0 # keystone 2.1.0 depends on pmtacceptance/stdlib >= 2.5.0 and pmtacceptance/mysql >=0.6.1 <1.0.0 before { preinstall('pmtacceptance-bacula', '0.0.1') } before { preinstall('pmtacceptance-mysql', '0.9.0') } before { preinstall('pmtacceptance-keystone', '2.1.0') } let(:module) { 'pmtacceptance-bacula' } it 'upgrades the module to the greatest version compatible with all other installed modules' do subject.should include :result => :success graph_should_include 'pmtacceptance-bacula', v('0.0.1') => v('0.0.2') end context 'using --force' do def options super.merge(:force => true) end it 'upgrades the module to the greatest version available' do subject.should include :result => :success graph_should_include 'pmtacceptance-bacula', v('0.0.1') => v('0.0.3') end end end context 'in other modulepath directories' do before { preinstall('pmtacceptance-apache', '0.0.3') } before { preinstall('pmtacceptance-stdlib', '1.0.0', :into => secondary_dir) } let(:module) { 'pmtacceptance-apache' } context 'with older major versions' do it 'upgrades the module to the greatest version compatible with the installed modules' do subject.should include :result => :success graph_should_include 'pmtacceptance-apache', v('0.0.3') => v('0.0.4') graph_should_include 'pmtacceptance-stdlib', nil end end context 'with satisfying major versions' do before { preinstall('pmtacceptance-stdlib', '2.0.0', :into => secondary_dir) } it 'upgrades the module and its dependencies to their greatest compatible versions, in-place' do subject.should include :result => :success graph_should_include 'pmtacceptance-apache', v('0.0.3') => v('0.10.0') graph_should_include 'pmtacceptance-stdlib', v('2.0.0') => v('2.6.0'), :path => secondary_dir end end end end end end end