diff --git a/acceptance/tests/modules/install/with_environment.rb b/acceptance/tests/modules/install/with_environment.rb new file mode 100644 index 000000000..fe032e5ec --- /dev/null +++ b/acceptance/tests/modules/install/with_environment.rb @@ -0,0 +1,38 @@ +test_name 'puppet module install (with environment)' + +step 'Setup' + +stub_forge_on(master) + +# Configure a non-default environment +on master, 'rm -rf /usr/share/puppet/modules /etc/puppet/testenv' +apply_manifest_on master, %q{ + file { + [ + '/usr/share/puppet/modules', + '/etc/puppet/testenv', + '/etc/puppet/testenv/modules', + ]: + ensure => directory, + } + file { + '/etc/puppet/puppet2.conf': + source => $settings::config, + } +} +on master, '{ echo "[testenv]"; echo "modulepath=/etc/puppet/testenv/modules"; } >> /etc/puppet/puppet2.conf' +teardown do +on master, 'rm -rf /usr/share/puppet/modules /etc/puppet/testenv /etc/puppet/puppet2.conf' +end + +step 'Install a module into a non default environment' +on master, 'puppet module install pmtacceptance-nginx --config=/etc/puppet/puppet2.conf --environment=testenv' do + assert_output <<-OUTPUT + \e[mNotice: Preparing to install into /etc/puppet/testenv/modules ...\e[0m + \e[mNotice: Downloading from https://forge.puppetlabs.com ...\e[0m + \e[mNotice: Installing -- do not interrupt ...\e[0m + /etc/puppet/testenv/modules + └── pmtacceptance-nginx (\e[0;36mv0.0.1\e[0m) + OUTPUT +end +on master, '[ -d /etc/puppet/testenv/modules/nginx ]' diff --git a/acceptance/tests/modules/uninstall/with_environment.rb b/acceptance/tests/modules/uninstall/with_environment.rb new file mode 100644 index 000000000..068bc5b9d --- /dev/null +++ b/acceptance/tests/modules/uninstall/with_environment.rb @@ -0,0 +1,47 @@ +test_name 'puppet module uninstall (with environment)' + +step 'Setup' + +stub_forge_on(master) + +# Configure a non-default environment +on master, 'rm -rf /usr/share/puppet/modules /etc/puppet/testenv' +apply_manifest_on master, %q{ + file { + [ + '/usr/share/puppet/modules', + '/etc/puppet/testenv', + '/etc/puppet/testenv/modules', + '/etc/puppet/testenv/modules/crakorn', + ]: + ensure => directory, + } + file { + '/etc/puppet/testenv/modules/crakorn/metadata.json': + content => '{ + "name": "jimmy/crakorn", + "version": "0.4.0", + "source": "", + "author": "jimmy", + "license": "MIT", + "dependencies": [] + }', + } + file { + '/etc/puppet/puppet2.conf': + source => $settings::config, + } +} +on master, '{ echo "[testenv]"; echo "modulepath=/etc/puppet/testenv/modules"; } >> /etc/puppet/puppet2.conf' +teardown do +on master, 'rm -rf /usr/share/puppet/modules /etc/puppet/testenv /etc/puppet/puppet2.conf' +end + +step 'Uninstall a module from a non default environment' +on master, 'puppet module uninstall jimmy-crakorn --config=/etc/puppet/puppet2.conf --environment=testenv' do + assert_output <<-OUTPUT + \e[mNotice: Preparing to uninstall 'jimmy-crakorn' ...\e[0m + Removed 'jimmy-crakorn' (\e[0;36mv0.4.0\e[0m) from /etc/puppet/testenv/modules + OUTPUT +end +on master, '[ ! -d /etc/puppet/testenv/modules/crakorn ]' diff --git a/lib/puppet/module_tool.rb b/lib/puppet/module_tool.rb index 30c96cc5b..4ff4207ec 100644 --- a/lib/puppet/module_tool.rb +++ b/lib/puppet/module_tool.rb @@ -1,135 +1,147 @@ # encoding: UTF-8 # Load standard libraries require 'pathname' require 'fileutils' require 'puppet/util/colors' module Puppet module ModuleTool extend Puppet::Util::Colors # Directory and names that should not be checksummed. ARTIFACTS = ['pkg', /^\./, /^~/, /^#/, 'coverage', 'metadata.json', 'REVISION'] FULL_MODULE_NAME_PATTERN = /\A([^-\/|.]+)[-|\/](.+)\z/ REPOSITORY_URL = Puppet.settings[:module_repository] # Is this a directory that shouldn't be checksummed? # # TODO: Should this be part of Checksums? # TODO: Rename this method to reflect it's purpose? # TODO: Shouldn't this be used when building packages too? def self.artifact?(path) case File.basename(path) when *ARTIFACTS true else false end end # Return the +username+ and +modname+ for a given +full_module_name+, or raise an # ArgumentError if the argument isn't parseable. def self.username_and_modname_from(full_module_name) if matcher = full_module_name.match(FULL_MODULE_NAME_PATTERN) return matcher.captures else raise ArgumentError, "Not a valid full name: #{full_module_name}" end end # Find the module root when given a path by checking each directory up from # its current location until it finds one that contains a file called # 'Modulefile'. # # @param path [Pathname, String] path to start from # @return [Pathname, nil] the root path of the module directory or nil if # we cannot find one def self.find_module_root(path) path = Pathname.new(path) if path.class == String path.expand_path.ascend do |p| return p if is_module_root?(p) end nil end # Analyse path to see if it is a module root directory by detecting a # file named 'Modulefile' in the directory. # # @param path [Pathname, String] path to analyse # @return [Boolean] true if the path is a module root, false otherwise def self.is_module_root?(path) path = Pathname.new(path) if path.class == String FileTest.file?(path + 'Modulefile') end # Builds a formatted tree from a list of node hashes containing +:text+ # and +:dependencies+ keys. def self.format_tree(nodes, level = 0) str = '' nodes.each_with_index do |node, i| last_node = nodes.length - 1 == i deps = node[:dependencies] || [] str << (indent = " " * level) str << (last_node ? "└" : "├") str << "─" str << (deps.empty? ? "─" : "┬") str << " #{node[:text]}\n" branch = format_tree(deps, level + 1) branch.gsub!(/^#{indent} /, indent + '│') unless last_node str << branch end return str end def self.build_tree(mods, dir) mods.each do |mod| version_string = mod[:version][:vstring].sub(/^(?!v)/, 'v') if mod[:action] == :upgrade previous_version = mod[:previous_version].sub(/^(?!v)/, 'v') version_string = "#{previous_version} -> #{version_string}" end mod[:text] = "#{mod[:module]} (#{colorize(:cyan, version_string)})" mod[:text] += " [#{mod[:path]}]" unless mod[:path] == dir build_tree(mod[:dependencies], dir) end end def self.set_option_defaults(options) sep = File::PATH_SEPARATOR - if options[:target_dir] - options[:target_dir] = File.expand_path(options[:target_dir]) - end - prepend_target_dir = !! options[:target_dir] + if options[:environment] + Puppet.settings[:environment] = options[:environment] + else + options[:environment] = Puppet.settings[:environment] + end - options[:modulepath] ||= Puppet.settings[:modulepath] - options[:environment] ||= Puppet.settings[:environment] - options[:modulepath] = "#{options[:target_dir]}#{sep}#{options[:modulepath]}" if prepend_target_dir - Puppet[:modulepath] = options[:modulepath] - Puppet[:environment] = options[:environment] + if options[:modulepath] + Puppet.settings[:modulepath] = options[:modulepath] + else + # (#14872) make sure the module path of the desired environment is used + # when determining the default value of the --target-dir option + Puppet.settings[:modulepath] = options[:modulepath] = + Puppet.settings.value(:modulepath, options[:environment]) + end - options[:target_dir] = options[:modulepath].split(sep).first - options[:target_dir] = File.expand_path(options[:target_dir]) + if options[:target_dir] + options[:target_dir] = File.expand_path(options[:target_dir]) + # prepend the target dir to the module path + Puppet.settings[:modulepath] = options[:modulepath] = + options[:target_dir] + sep + options[:modulepath] + else + # default to the first component of the module path + options[:target_dir] = + File.expand_path(options[:modulepath].split(sep).first) + end end end end # Load remaining libraries require 'puppet/module_tool/errors' require 'puppet/module_tool/applications' require 'puppet/module_tool/checksums' require 'puppet/module_tool/contents_description' require 'puppet/module_tool/dependency' require 'puppet/module_tool/metadata' require 'puppet/module_tool/modulefile' require 'puppet/module_tool/skeleton' require 'puppet/forge/cache' require 'puppet/forge' diff --git a/spec/unit/module_tool_spec.rb b/spec/unit/module_tool_spec.rb index 0343984d6..93db99afe 100755 --- a/spec/unit/module_tool_spec.rb +++ b/spec/unit/module_tool_spec.rb @@ -1,244 +1,277 @@ #! /usr/bin/env ruby # encoding: UTF-8 require 'spec_helper' require 'puppet/module_tool' describe Puppet::ModuleTool do describe '.is_module_root?' do it 'should return true if directory has a module file' do FileTest.expects(:file?).with(responds_with(:to_s, '/a/b/c/Modulefile')). returns(true) subject.is_module_root?(Pathname.new('/a/b/c')).should be_true end it 'should return false if directory does not have a module file' do FileTest.expects(:file?).with(responds_with(:to_s, '/a/b/c/Modulefile')). returns(false) subject.is_module_root?(Pathname.new('/a/b/c')).should be_false end end describe '.find_module_root' do let(:sample_path) { Pathname.new('/a/b/c').expand_path } it 'should return the first path as a pathname when it contains a module file' do Puppet::ModuleTool.expects(:is_module_root?).with(sample_path). returns(true) subject.find_module_root(sample_path).should == sample_path end it 'should return a parent path as a pathname when it contains a module file' do Puppet::ModuleTool.expects(:is_module_root?). with(responds_with(:to_s, File.expand_path('/a/b/c'))).returns(false) Puppet::ModuleTool.expects(:is_module_root?). with(responds_with(:to_s, File.expand_path('/a/b'))).returns(true) subject.find_module_root(sample_path).should == Pathname.new('/a/b').expand_path end it 'should return nil when no module root can be found' do Puppet::ModuleTool.expects(:is_module_root?).at_least_once.returns(false) subject.find_module_root(sample_path).should be_nil end end describe '.format_tree' do it 'should return an empty tree when given an empty list' do subject.format_tree([]).should == '' end it 'should return a shallow when given a list without dependencies' do list = [ { :text => 'first' }, { :text => 'second' }, { :text => 'third' } ] subject.format_tree(list).should == <<-TREE ├── first ├── second └── third TREE end it 'should return a deeply nested tree when given a list with deep dependencies' do list = [ { :text => 'first', :dependencies => [ { :text => 'second', :dependencies => [ { :text => 'third' } ] } ] }, ] subject.format_tree(list).should == <<-TREE └─┬ first └─┬ second └── third TREE end it 'should show connectors when deep dependencies are not on the last node of the top level' do list = [ { :text => 'first', :dependencies => [ { :text => 'second', :dependencies => [ { :text => 'third' } ] } ] }, { :text => 'fourth' } ] subject.format_tree(list).should == <<-TREE ├─┬ first │ └─┬ second │ └── third └── fourth TREE end it 'should show connectors when deep dependencies are not on the last node of any level' do list = [ { :text => 'first', :dependencies => [ { :text => 'second', :dependencies => [ { :text => 'third' } ] }, { :text => 'fourth' } ] } ] subject.format_tree(list).should == <<-TREE └─┬ first ├─┬ second │ └── third └── fourth TREE end it 'should show connectors in every case when deep dependencies are not on the last node' do list = [ { :text => 'first', :dependencies => [ { :text => 'second', :dependencies => [ { :text => 'third' } ] }, { :text => 'fourth' } ] }, { :text => 'fifth' } ] subject.format_tree(list).should == <<-TREE ├─┬ first │ ├─┬ second │ │ └── third │ └── fourth └── fifth TREE end end + describe '.set_option_defaults' do - include PuppetSpec::Files - let (:setting) { {:environment => "foo", :modulepath => make_absolute("foo")} } + describe 'option :environment' do + context 'passed:' do + let (:environment) { "ahgkduerh" } + let (:options) { {:environment => environment} } - [:environment, :modulepath].each do |value| - describe "if #{value} is part of options" do - let (:options) { {} } + it 'Puppet[:environment] should be set to the value of the option' do + subject.set_option_defaults options - before(:each) do - options[value] = setting[value] - Puppet[value] = "bar" + Puppet[:environment].should == environment end - it "should set Puppet[#{value}] to the options[#{value}]" do + it 'the option value should not be overridden' do + Puppet[:environment] = :foo subject.set_option_defaults options - Puppet[value].should == options[value] + + options[:environment].should == environment + end + end + + context 'NOT passed:' do + it 'Puppet[:environment] should NOT be overridden' do + Puppet[:environment] = :foo + + subject.set_option_defaults({}) + Puppet[:environment].should == :foo end - it "should not override options[#{value}]" do + it 'the option should be set to the value of Puppet[:environment]' do + options_to_modify = Hash.new + Puppet[:environment] = :abcdefg + + subject.set_option_defaults options_to_modify + + options_to_modify[:environment].should == :abcdefg + end + end + end + + describe 'option :modulepath' do + context 'passed:' do + let (:modulepath) { PuppetSpec::Files.make_absolute('/bar') } + let (:options) { {:modulepath => modulepath} } + + it 'Puppet[:modulepath] should be set to the value of the option' do + subject.set_option_defaults options - options[value].should == setting[value] + + Puppet[:modulepath].should == modulepath end + it 'the option value should not be overridden' do + Puppet[:modulepath] = "/foo" + + subject.set_option_defaults options + + options[:modulepath].should == modulepath + end end - describe "if #{value} is not part of options" do - let (:options) { {} } + context 'NOT passed:' do + let (:options) { {:environment => :pmttestenv} } before(:each) do - Puppet[value] = setting[value] + Puppet[:modulepath] = "/no" + Puppet[:environment] = :pmttestenv + Puppet.settings.set_value(:modulepath, + ["/foo", "/bar", "/no"].join(File::PATH_SEPARATOR), + :pmttestenv) end - it "should populate options[#{value}] with the value of Puppet[#{value}]" do + it 'Puppet[:modulepath] should be reset to the module path of the current environment' do subject.set_option_defaults options - Puppet[value].should == options[value] + + Puppet[:modulepath].should == Puppet.settings.value(:modulepath, :pmttestenv) end - it "should not override Puppet[#{value}]" do + it 'the option should be set to the module path of the current environment' do subject.set_option_defaults options - Puppet[value].should == setting[value] + + options[:modulepath].should == Puppet.settings.value(:modulepath, :pmttestenv) end end end - describe ':target_dir' do - let (:sep) { File::PATH_SEPARATOR } + describe 'option :target_dir' do + let (:target_dir) { 'boo' } - let (:my_fake_path) { - ["/my/fake/dir", "/my/other/dir"].collect { |dir| make_absolute(dir) } .join(sep) - } - let (:options) { {:modulepath => my_fake_path}} + context 'passed:' do + let (:options) { {:target_dir => target_dir} } - describe "when not specified" do + it 'the option value should be prepended to the Puppet[:modulepath]' do + Puppet[:modulepath] = "/fuz" + original_modulepath = Puppet[:modulepath] - it "should set options[:target_dir]" do subject.set_option_defaults options - options[:target_dir].should_not be_nil + + Puppet[:modulepath].should == options[:target_dir] + File::PATH_SEPARATOR + original_modulepath end - it "should be the first path of options[:modulepath]" do + it 'the option value should be turned into an absolute path' do subject.set_option_defaults options - options[:target_dir].should == my_fake_path.split(sep).first - end - end - describe "when specified" do - let (:my_target_dir) { make_absolute("/foo/bar") } - before(:each) do - options[:target_dir] = my_target_dir + options[:target_dir].should == File.expand_path(target_dir) end + end - it "should not be overridden" do - subject.set_option_defaults options - options[:target_dir].should == my_target_dir + describe 'NOT passed:' do + before :each do + Puppet[:modulepath] = 'foo' + File::PATH_SEPARATOR + 'bar' end - it "should be prepended to options[:modulepath]" do + it 'the option should be set to the first component of Puppet[:modulepath]' do + options = Hash.new subject.set_option_defaults options - options[:modulepath].split(sep).first.should == my_target_dir - end - it "should leave the remainder of options[:modulepath] untouched" do - subject.set_option_defaults options - options[:modulepath].split(sep).drop(1).join(sep).should == my_fake_path + options[:target_dir].should == Puppet[:modulepath].split(File::PATH_SEPARATOR)[0] end end - end end end