diff --git a/acceptance/tests/modules/install/already_installed.rb b/acceptance/tests/modules/install/already_installed.rb
new file mode 100644
index 000000000..70abc8506
--- /dev/null
+++ b/acceptance/tests/modules/install/already_installed.rb
@@ -0,0 +1,63 @@
+begin test_name "puppet module install (already installed)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules/nginx',
+ ]: ensure => directory;
+ '/etc/puppet/modules/nginx/metadata.json':
+ content => '{
+ "name": "pmtacceptance/nginx",
+ "version": "0.0.1",
+ "source": "",
+ "author": "pmtacceptance",
+ "license": "MIT",
+ "dependencies": []
+ }';
+}
+PP
+
+step "Try to install a module that is already installed"
+on master, puppet("module install pmtacceptance-nginx"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-nginx' (latest)
+ STDERR> Module 'pmtacceptance-nginx' (v0.0.1) is already installed
+ STDERR> Use `puppet module upgrade` to install a different version
+ STDERR> Use `puppet module install --force` to re-install only this module\e[0m
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/nginx ]'
+
+step "Try to install a specific version of a module that is already installed"
+on master, puppet("module install pmtacceptance-nginx --version 1.x"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-nginx' (v1.x)
+ STDERR> Module 'pmtacceptance-nginx' (v0.0.1) is already installed
+ STDERR> Use `puppet module upgrade` to install a different version
+ STDERR> Use `puppet module install --force` to re-install only this module\e[0m
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/nginx ]'
+
+step "Install a module that is already installed (with --force)"
+on master, puppet("module install pmtacceptance-nginx --force") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-nginx (\e[0;36mv0.0.1\e[0m)
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/nginx ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { '/etc/puppet/modules': recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/install/already_installed_elsewhere.rb b/acceptance/tests/modules/install/already_installed_elsewhere.rb
new file mode 100644
index 000000000..eee1a5f1c
--- /dev/null
+++ b/acceptance/tests/modules/install/already_installed_elsewhere.rb
@@ -0,0 +1,66 @@
+begin test_name "puppet module install (already installed elsewhere)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules',
+ '/usr/share/puppet',
+ '/usr/share/puppet/modules',
+ '/usr/share/puppet/modules/nginx',
+ ]: ensure => directory;
+ '/usr/share/puppet/modules/nginx/metadata.json':
+ content => '{
+ "name": "pmtacceptance/nginx",
+ "version": "0.0.1",
+ "source": "",
+ "author": "pmtacceptance",
+ "license": "MIT",
+ "dependencies": []
+ }';
+}
+PP
+
+step "Try to install a module that is already installed"
+on master, puppet("module install pmtacceptance-nginx"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-nginx' (latest)
+ STDERR> Module 'pmtacceptance-nginx' (v0.0.1) is already installed
+ STDERR> Use `puppet module upgrade` to install a different version
+ STDERR> Use `puppet module install --force` to re-install only this module\e[0m
+ OUTPUT
+end
+on master, '[ ! -d /etc/puppet/modules/nginx ]'
+
+step "Try to install a specific version of a module that is already installed"
+on master, puppet("module install pmtacceptance-nginx --version 1.x"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-nginx' (v1.x)
+ STDERR> Module 'pmtacceptance-nginx' (v0.0.1) is already installed
+ STDERR> Use `puppet module upgrade` to install a different version
+ STDERR> Use `puppet module install --force` to re-install only this module\e[0m
+ OUTPUT
+end
+on master, '[ ! -d /etc/puppet/modules/nginx ]'
+
+step "Install a module that is already installed (with --force)"
+on master, puppet("module install pmtacceptance-nginx --force") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-nginx (\e[0;36mv0.0.1\e[0m)
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/nginx ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { '/etc/puppet/modules': recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/install/already_installed_with_local_changes.rb b/acceptance/tests/modules/install/already_installed_with_local_changes.rb
new file mode 100644
index 000000000..55b46c477
--- /dev/null
+++ b/acceptance/tests/modules/install/already_installed_with_local_changes.rb
@@ -0,0 +1,70 @@
+begin test_name "puppet module install (already installed with local changes)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules/nginx',
+ ]: ensure => directory;
+ '/etc/puppet/modules/nginx/metadata.json':
+ content => '{
+ "name": "pmtacceptance/nginx",
+ "version": "0.0.1",
+ "source": "",
+ "author": "pmtacceptance",
+ "license": "MIT",
+ "checksums": {
+ "README": "2a3adc3b053ef1004df0a02cefbae31f"
+ },
+ "dependencies": []
+ }';
+ '/etc/puppet/modules/nginx/README':
+ content => 'Nginx module';
+}
+PP
+
+step "Try to install a module that is already installed"
+on master, puppet("module install pmtacceptance-nginx"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-nginx' (latest)
+ STDERR> Module 'pmtacceptance-nginx' (v0.0.1) is already installed
+ STDERR> Installed module has had changes made locally
+ STDERR> Use `puppet module upgrade` to install a different version
+ STDERR> Use `puppet module install --force` to re-install only this module\e[0m
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/nginx ]'
+
+step "Try to install a specific version of a module that is already installed"
+on master, puppet("module install pmtacceptance-nginx --version 1.x"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-nginx' (v1.x)
+ STDERR> Module 'pmtacceptance-nginx' (v0.0.1) is already installed
+ STDERR> Installed module has had changes made locally
+ STDERR> Use `puppet module upgrade` to install a different version
+ STDERR> Use `puppet module install --force` to re-install only this module\e[0m
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/nginx ]'
+
+step "Install a module that is already installed (with --force)"
+on master, puppet("module install pmtacceptance-nginx --force") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-nginx (\e[0;36mv0.0.1\e[0m)
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/nginx ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { '/etc/puppet/modules': recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/install/force_ignores_dependencies.rb b/acceptance/tests/modules/install/force_ignores_dependencies.rb
new file mode 100644
index 000000000..a8feea158
--- /dev/null
+++ b/acceptance/tests/modules/install/force_ignores_dependencies.rb
@@ -0,0 +1,40 @@
+begin test_name "puppet module install (force ignores dependencies)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+
+step "Try to install an unsatisfiable module"
+on master, puppet("module install pmtacceptance-php"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-php' (latest: v0.0.2)
+ STDERR> No version of 'pmtacceptance-php' will satisfy dependencies
+ STDERR> You specified 'pmtacceptance-php' (latest: v0.0.2),
+ STDERR> which depends on 'pmtacceptance-apache' (v0.0.1),
+ STDERR> which depends on 'pmtacceptance-php' (v0.0.1)
+ STDERR> Use `puppet module install --force` to install this module anyway\e[0m
+ OUTPUT
+end
+on master, '[ ! -d /etc/puppet/modules/php ]'
+on master, '[ ! -d /etc/puppet/modules/apache ]'
+
+step "Install an unsatisfiable module with force"
+on master, puppet("module install pmtacceptance-php --force") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-php (\e[0;36mv0.0.2\e[0m)
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/php ]'
+on master, '[ ! -d /etc/puppet/modules/apache ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { '/etc/puppet/modules': recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/install/ignoring_dependencies.rb b/acceptance/tests/modules/install/ignoring_dependencies.rb
new file mode 100644
index 000000000..791383354
--- /dev/null
+++ b/acceptance/tests/modules/install/ignoring_dependencies.rb
@@ -0,0 +1,24 @@
+begin test_name "puppet module install (ignoring dependencies)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+
+step "Install a module, but ignore dependencies"
+on master, puppet("module install pmtacceptance-java --ignore-dependencies") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-java (\e[0;36mv1.7.1\e[0m)
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/java ]'
+on master, '[ ! -d /etc/puppet/modules/stdlib ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { '/etc/puppet/modules': recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/install/nonexistent_directory.rb b/acceptance/tests/modules/install/nonexistent_directory.rb
new file mode 100644
index 000000000..6789a86c3
--- /dev/null
+++ b/acceptance/tests/modules/install/nonexistent_directory.rb
@@ -0,0 +1,40 @@
+begin test_name "puppet module install (nonexistent directory)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules',
+ '/tmp/modules',
+ ]: ensure => absent, recurse => true, force => true;
+}
+PP
+
+step "Try to install a module to a non-existent directory"
+on master, puppet("module install pmtacceptance-nginx --target-dir /tmp/modules"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /tmp/modules ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-nginx' (latest)
+ STDERR> Directory /tmp/modules does not exist\e[0m
+ OUTPUT
+end
+on master, '[ ! -d /etc/puppet/modules/nginx ]'
+
+step "Try to install a module to a non-existent implicit directory"
+on master, puppet("module install pmtacceptance-nginx"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-nginx' (latest)
+ STDERR> Directory /etc/puppet/modules does not exist\e[0m
+ OUTPUT
+end
+on master, '[ ! -d /etc/puppet/modules/nginx ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { '/etc/puppet/modules': ensure => directory }"
+apply_manifest_on master, "file { '/etc/puppet/modules': recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/install/with_cycles.rb b/acceptance/tests/modules/install/with_cycles.rb
new file mode 100644
index 000000000..d7d27bd04
--- /dev/null
+++ b/acceptance/tests/modules/install/with_cycles.rb
@@ -0,0 +1,32 @@
+begin test_name "puppet module install (with cycles)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+
+step "Install a module with cycles"
+on master, puppet("module install pmtacceptance-php --version 0.0.1") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └─┬ pmtacceptance-php (\e[0;36mv0.0.1\e[0m)
+ └── pmtacceptance-apache (\e[0;36mv0.0.1\e[0m)
+ OUTPUT
+end
+
+on master, puppet('module list') do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-apache (\e[0;36mv0.0.1\e[0m)
+ └── pmtacceptance-php (\e[0;36mv0.0.1\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/install/with_dependencies.rb b/acceptance/tests/modules/install/with_dependencies.rb
new file mode 100644
index 000000000..f0d87f6b7
--- /dev/null
+++ b/acceptance/tests/modules/install/with_dependencies.rb
@@ -0,0 +1,25 @@
+begin test_name "puppet module install (with dependencies)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+
+step "Install a module with dependencies"
+on master, puppet("module install pmtacceptance-java") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └─┬ pmtacceptance-java (\e[0;36mv1.7.1\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/java ]'
+on master, '[ -d /etc/puppet/modules/stdlib ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/install/with_existing_module_directory.rb b/acceptance/tests/modules/install/with_existing_module_directory.rb
new file mode 100644
index 000000000..a21fbd1b6
--- /dev/null
+++ b/acceptance/tests/modules/install/with_existing_module_directory.rb
@@ -0,0 +1,96 @@
+begin test_name "puppet module install (with existing module directory)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules/nginx',
+ '/etc/puppet/modules/apache',
+ ]: ensure => directory;
+ '/etc/puppet/modules/nginx/metadata.json':
+ content => '{
+ "name": "notpmtacceptance/nginx",
+ "version": "0.0.3",
+ "source": "",
+ "author": "notpmtacceptance",
+ "license": "MIT",
+ "dependencies": []
+ }';
+ [
+ '/etc/puppet/modules/nginx/extra.json',
+ '/etc/puppet/modules/apache/extra.json',
+ ]: content => '';
+}
+PP
+
+step "Try to install an module with a name collision"
+on master, puppet("module install pmtacceptance-nginx"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-nginx' (latest: v0.0.1)
+ STDERR> Installation would overwrite /etc/puppet/modules/nginx
+ STDERR> Currently, 'notpmtacceptance-nginx' (v0.0.3) is installed to that directory
+ STDERR> Use `puppet module install --dir
` to install modules elsewhere
+ STDERR> Use `puppet module install --force` to install this module anyway\e[0m
+ OUTPUT
+end
+on master, '[ -f /etc/puppet/modules/nginx/extra.json ]'
+
+step "Try to install an module with a path collision"
+on master, puppet("module install pmtacceptance-apache"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-apache' (latest: v0.0.1)
+ STDERR> Installation would overwrite /etc/puppet/modules/apache
+ STDERR> Use `puppet module install --dir ` to install modules elsewhere
+ STDERR> Use `puppet module install --force` to install this module anyway\e[0m
+ OUTPUT
+end
+on master, '[ -f /etc/puppet/modules/apache/extra.json ]'
+
+step "Try to install an module with a dependency that has collides"
+on master, puppet("module install pmtacceptance-php --version 0.0.1"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-php' (v0.0.1)
+ STDERR> Dependency 'pmtacceptance-apache' (v0.0.1) would overwrite /etc/puppet/modules/apache
+ STDERR> Use `puppet module install --dir ` to install modules elsewhere
+ STDERR> Use `puppet module install --ignore-dependencies` to install only this module\e[0m
+ OUTPUT
+end
+on master, '[ -f /etc/puppet/modules/apache/extra.json ]'
+
+step "Install an module with a name collision by using --force"
+on master, puppet("module install pmtacceptance-nginx --force"), :acceptable_exit_codes => [0] do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-nginx (\e[0;36mv0.0.1\e[0m)
+ OUTPUT
+end
+on master, '[ ! -f /etc/puppet/modules/nginx/extra.json ]'
+
+step "Install an module with a name collision by using --force"
+on master, puppet("module install pmtacceptance-apache --force"), :acceptable_exit_codes => [0] do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-apache (\e[0;36mv0.0.1\e[0m)
+ OUTPUT
+end
+on master, '[ ! -f /etc/puppet/modules/apache/extra.json ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { '/etc/puppet/modules': recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/install/with_necessary_upgrade.rb b/acceptance/tests/modules/install/with_necessary_upgrade.rb
new file mode 100644
index 000000000..453dbf147
--- /dev/null
+++ b/acceptance/tests/modules/install/with_necessary_upgrade.rb
@@ -0,0 +1,55 @@
+begin test_name "puppet module install (with necessary dependency upgrade)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+
+step "Install an older module version"
+on master, puppet("module install pmtacceptance-java --version 1.6.0") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └─┬ pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ OUTPUT
+end
+
+on master, puppet('module list --tree') do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ └─┬ pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+
+step "Install a module that requires the older module dependency be upgraded"
+on master, puppet("module install pmtacceptance-apollo") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └─┬ pmtacceptance-apollo (\e[0;36mv0.0.1\e[0m)
+ └── pmtacceptance-java (\e[0;36mv1.6.0 -> v1.7.1\e[0m)
+ OUTPUT
+end
+
+on master, puppet('module list') do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-apollo (\e[0;36mv0.0.1\e[0m)
+ ├── pmtacceptance-java (\e[0;36mv1.7.1\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/install/with_no_dependencies.rb b/acceptance/tests/modules/install/with_no_dependencies.rb
new file mode 100644
index 000000000..ca2aa1d3a
--- /dev/null
+++ b/acceptance/tests/modules/install/with_no_dependencies.rb
@@ -0,0 +1,23 @@
+begin test_name "puppet module install (with no dependencies)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+
+step "Install a module with no dependencies"
+on master, puppet("module install pmtacceptance-nginx") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-nginx (\e[0;36mv0.0.1\e[0m)
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/nginx ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/install/with_unnecessary_upgrade.rb b/acceptance/tests/modules/install/with_unnecessary_upgrade.rb
new file mode 100644
index 000000000..532f1f012
--- /dev/null
+++ b/acceptance/tests/modules/install/with_unnecessary_upgrade.rb
@@ -0,0 +1,54 @@
+begin test_name "puppet module install (with unnecessary dependency upgrade)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+
+step "Install an older module version"
+on master, puppet("module install pmtacceptance-java --version 1.7.0") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └─┬ pmtacceptance-java (\e[0;36mv1.7.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ OUTPUT
+end
+
+on master, puppet('module list') do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.7.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+
+step "Install a module that depends on a dependency that could be upgraded, but already satisfies constraints"
+on master, puppet("module install pmtacceptance-apollo") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-apollo (\e[0;36mv0.0.1\e[0m)
+ OUTPUT
+end
+
+on master, puppet('module list') do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-apollo (\e[0;36mv0.0.1\e[0m)
+ ├── pmtacceptance-java (\e[0;36mv1.7.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/install/with_unsatisfied_constraints.rb b/acceptance/tests/modules/install/with_unsatisfied_constraints.rb
new file mode 100644
index 000000000..16ddd1c34
--- /dev/null
+++ b/acceptance/tests/modules/install/with_unsatisfied_constraints.rb
@@ -0,0 +1,97 @@
+begin test_name "puppet module install (with unsatisfied constraints)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules/crakorn',
+ ]: ensure => directory;
+ '/etc/puppet/modules/crakorn/metadata.json':
+ content => '{
+ "name": "jimmy/crakorn",
+ "version": "0.0.1",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "pmtacceptance/stdlib", "version_requirement": "1.x" }
+ ]
+ }';
+}
+PP
+
+step "Try to install a module that has an unsatisfiable dependency"
+on master, puppet("module install pmtacceptance-git"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-git' (latest: v0.0.1)
+ STDERR> No version of 'pmtacceptance-stdlib' will satisfy dependencies
+ STDERR> 'jimmy-crakorn' (v0.0.1) requires 'pmtacceptance-stdlib' (v1.x)
+ STDERR> 'pmtacceptance-git' (v0.0.1) requires 'pmtacceptance-stdlib' (>= 2.0.0)
+ STDERR> Use `puppet module install --ignore-dependencies` to install only this module\e[0m
+ OUTPUT
+end
+on master, '[ ! -d /etc/puppet/modules/git ]'
+
+step "Install the module with an unsatisfiable dependency"
+on master, puppet("module install pmtacceptance-git --ignore-dependencies") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-git (\e[0;36mv0.0.1\e[0m)
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/git ]'
+
+step "Try to install a specific version of the unsatisfiable dependency"
+on master, puppet("module install pmtacceptance-stdlib --version 1.x"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-stdlib' (v1.x)
+ STDERR> No version of 'pmtacceptance-stdlib' will satisfy dependencies
+ STDERR> You specified 'pmtacceptance-stdlib' (v1.x)
+ STDERR> 'jimmy-crakorn' (v0.0.1) requires 'pmtacceptance-stdlib' (v1.x)
+ STDERR> 'pmtacceptance-git' (v0.0.1) requires 'pmtacceptance-stdlib' (>= 2.0.0)
+ STDERR> Use `puppet module install --force` to install this module anyway\e[0m
+ OUTPUT
+end
+on master, '[ ! -d /etc/puppet/modules/stdlib ]'
+
+step "Try to install any version of the unsatisfiable dependency"
+on master, puppet("module install pmtacceptance-stdlib"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-stdlib' (best: v1.0.0)
+ STDERR> No version of 'pmtacceptance-stdlib' will satisfy dependencies
+ STDERR> You specified 'pmtacceptance-stdlib' (best: v1.0.0)
+ STDERR> 'jimmy-crakorn' (v0.0.1) requires 'pmtacceptance-stdlib' (v1.x)
+ STDERR> 'pmtacceptance-git' (v0.0.1) requires 'pmtacceptance-stdlib' (>= 2.0.0)
+ STDERR> Use `puppet module install --force` to install this module anyway\e[0m
+ OUTPUT
+end
+on master, '[ ! -d /etc/puppet/modules/stdlib ]'
+
+step "Install the unsatisfiable dependency with --force"
+on master, puppet("module install pmtacceptance-stdlib --force") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/stdlib ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { '/etc/puppet/modules': recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/list/with_circular_dependencies.rb b/acceptance/tests/modules/list/with_circular_dependencies.rb
new file mode 100644
index 000000000..499b0446b
--- /dev/null
+++ b/acceptance/tests/modules/list/with_circular_dependencies.rb
@@ -0,0 +1,69 @@
+begin test_name "puppet module list (with circular dependencies)"
+
+step "Setup"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules',
+ '/etc/puppet/modules/appleseed',
+ '/usr/share/puppet',
+ '/usr/share/puppet/modules',
+ '/usr/share/puppet/modules/crakorn',
+ ]: ensure => directory,
+ recurse => true,
+ purge => true,
+ force => true;
+ '/usr/share/puppet/modules/crakorn/metadata.json':
+ content => '{
+ "name": "jimmy/crakorn",
+ "version": "0.4.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/appleseed", "version_requirement": "1.1.0" }
+ ]
+ }';
+ '/etc/puppet/modules/appleseed/metadata.json':
+ content => '{
+ "name": "jimmy/appleseed",
+ "version": "1.1.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/crakorn", "version_requirement": "0.4.0" }
+ ]
+ }';
+}
+PP
+on master, '[ -d /etc/puppet/modules/appleseed ]'
+on master, '[ -d /usr/share/puppet/modules/crakorn ]'
+
+step "List the installed modules"
+on master, puppet('module list') do
+ assert_equal '', stderr
+ assert_equal <<-STDOUT, stdout
+/etc/puppet/modules
+└── jimmy-appleseed (\e[0;36mv1.1.0\e[0m)
+/usr/share/puppet/modules
+└── jimmy-crakorn (\e[0;36mv0.4.0\e[0m)
+STDOUT
+end
+
+step "List the installed modules as a dependency tree"
+on master, puppet('module list --tree') do
+ assert_equal '', stderr
+ assert_equal <<-STDOUT, stdout
+/etc/puppet/modules
+└─┬ jimmy-appleseed (\e[0;36mv1.1.0\e[0m)
+ └── jimmy-crakorn (\e[0;36mv0.4.0\e[0m) [/usr/share/puppet/modules]
+/usr/share/puppet/modules
+└─┬ jimmy-crakorn (\e[0;36mv0.4.0\e[0m)
+ └── jimmy-appleseed (\e[0;36mv1.1.0\e[0m) [/etc/puppet/modules]
+STDOUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/list/with_installed_modules.rb b/acceptance/tests/modules/list/with_installed_modules.rb
new file mode 100644
index 000000000..1014c2f62
--- /dev/null
+++ b/acceptance/tests/modules/list/with_installed_modules.rb
@@ -0,0 +1,96 @@
+begin test_name "puppet module list (with installed modules)"
+
+step "Setup"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules',
+ '/etc/puppet/modules/crakorn',
+ '/etc/puppet/modules/appleseed',
+ '/etc/puppet/modules/thelock',
+ '/usr/share/puppet',
+ '/usr/share/puppet/modules',
+ '/usr/share/puppet/modules/crick',
+ ]: ensure => directory,
+ recurse => true,
+ purge => true,
+ force => true;
+ '/etc/puppet/modules/crakorn/metadata.json':
+ content => '{
+ "name": "jimmy/crakorn",
+ "version": "0.4.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": []
+ }';
+ '/etc/puppet/modules/appleseed/metadata.json':
+ content => '{
+ "name": "jimmy/appleseed",
+ "version": "1.1.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/crakorn", "version_requirement": "0.4.0" }
+ ]
+ }';
+ '/etc/puppet/modules/thelock/metadata.json':
+ content => '{
+ "name": "jimmy/thelock",
+ "version": "1.0.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/appleseed", "version_requirement": "1.x" }
+ ]
+ }';
+ '/usr/share/puppet/modules/crick/metadata.json':
+ content => '{
+ "name": "jimmy/crick",
+ "version": "1.0.1",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/crakorn", "version_requirement": "0.4.x" }
+ ]
+ }';
+}
+PP
+on master, '[ -d /etc/puppet/modules/crakorn ]'
+on master, '[ -d /etc/puppet/modules/appleseed ]'
+on master, '[ -d /etc/puppet/modules/thelock ]'
+on master, '[ -d /usr/share/puppet/modules/crick ]'
+
+step "List the installed modules"
+on master, puppet('module list') do
+ assert_equal '', stderr
+ assert_equal <<-STDOUT, stdout
+/etc/puppet/modules
+├── jimmy-appleseed (\e[0;36mv1.1.0\e[0m)
+├── jimmy-crakorn (\e[0;36mv0.4.0\e[0m)
+└── jimmy-thelock (\e[0;36mv1.0.0\e[0m)
+/usr/share/puppet/modules
+└── jimmy-crick (\e[0;36mv1.0.1\e[0m)
+STDOUT
+end
+
+step "List the installed modules as a dependency tree"
+on master, puppet('module list --tree') do
+ assert_equal '', stderr
+ assert_equal <<-STDOUT, stdout
+/etc/puppet/modules
+└─┬ jimmy-thelock (\e[0;36mv1.0.0\e[0m)
+ └─┬ jimmy-appleseed (\e[0;36mv1.1.0\e[0m)
+ └── jimmy-crakorn (\e[0;36mv0.4.0\e[0m)
+/usr/share/puppet/modules
+└─┬ jimmy-crick (\e[0;36mv1.0.1\e[0m)
+ └── jimmy-crakorn (\e[0;36mv0.4.0\e[0m) [/etc/puppet/modules]
+STDOUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/list/with_invalid_dependencies.rb b/acceptance/tests/modules/list/with_invalid_dependencies.rb
new file mode 100644
index 000000000..93f5da8f4
--- /dev/null
+++ b/acceptance/tests/modules/list/with_invalid_dependencies.rb
@@ -0,0 +1,102 @@
+begin test_name "puppet module list (with invalid dependencies)"
+
+step "Setup"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules',
+ '/etc/puppet/modules/appleseed',
+ '/etc/puppet/modules/crakorn',
+ '/etc/puppet/modules/thelock',
+ '/usr/share/puppet',
+ '/usr/share/puppet/modules',
+ '/usr/share/puppet/modules/crick',
+ ]: ensure => directory,
+ recurse => true,
+ purge => true,
+ force => true;
+ '/etc/puppet/modules/crakorn/metadata.json':
+ content => '{
+ "name": "jimmy/crakorn",
+ "version": "0.3.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": []
+ }';
+ '/etc/puppet/modules/appleseed/metadata.json':
+ content => '{
+ "name": "jimmy/appleseed",
+ "version": "1.1.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/crakorn", "version_requirement": "0.x" }
+ ]
+ }';
+ '/etc/puppet/modules/thelock/metadata.json':
+ content => '{
+ "name": "jimmy/thelock",
+ "version": "1.0.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/appleseed", "version_requirement": "1.x" }
+ ]
+ }';
+ '/usr/share/puppet/modules/crick/metadata.json':
+ content => '{
+ "name": "jimmy/crick",
+ "version": "1.0.1",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/crakorn", "version_requirement": "0.4.x" }
+ ]
+ }';
+}
+PP
+on master, '[ -d /etc/puppet/modules/appleseed ]'
+on master, '[ -d /etc/puppet/modules/crakorn ]'
+on master, '[ -d /etc/puppet/modules/thelock ]'
+on master, '[ -d /usr/share/puppet/modules/crick ]'
+
+step "List the installed modules"
+on master, puppet('module list') do
+ assert_equal <<-STDERR, stderr
+\e[1;31mWarning: Module 'jimmy-crakorn' (v0.3.0) fails to meet some dependencies:
+ 'jimmy-crick' (v1.0.1) requires 'jimmy-crakorn' (v0.4.x)\e[0m
+STDERR
+ assert_equal <<-STDOUT, stdout
+/etc/puppet/modules
+├── jimmy-appleseed (\e[0;36mv1.1.0\e[0m)
+├── jimmy-crakorn (\e[0;36mv0.3.0\e[0m) \e[0;31minvalid\e[0m
+└── jimmy-thelock (\e[0;36mv1.0.0\e[0m)
+/usr/share/puppet/modules
+└── jimmy-crick (\e[0;36mv1.0.1\e[0m)
+STDOUT
+end
+
+step "List the installed modules as a dependency tree"
+on master, puppet('module list --tree') do
+ assert_equal <<-STDERR, stderr
+\e[1;31mWarning: Module 'jimmy-crakorn' (v0.3.0) fails to meet some dependencies:
+ 'jimmy-crick' (v1.0.1) requires 'jimmy-crakorn' (v0.4.x)\e[0m
+STDERR
+ assert_equal <<-STDOUT, stdout
+/etc/puppet/modules
+└─┬ jimmy-thelock (\e[0;36mv1.0.0\e[0m)
+ └─┬ jimmy-appleseed (\e[0;36mv1.1.0\e[0m)
+ └── jimmy-crakorn (\e[0;36mv0.3.0\e[0m)
+/usr/share/puppet/modules
+└─┬ jimmy-crick (\e[0;36mv1.0.1\e[0m)
+ └── jimmy-crakorn (\e[0;36mv0.3.0\e[0m) [/etc/puppet/modules] \e[0;31minvalid\e[0m
+STDOUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/list/with_missing_dependencies.rb b/acceptance/tests/modules/list/with_missing_dependencies.rb
new file mode 100644
index 000000000..fdf029961
--- /dev/null
+++ b/acceptance/tests/modules/list/with_missing_dependencies.rb
@@ -0,0 +1,98 @@
+begin test_name "puppet module list (with missing dependencies)"
+
+step "Setup"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules',
+ '/etc/puppet/modules/appleseed',
+ '/etc/puppet/modules/thelock',
+ '/usr/share/puppet',
+ '/usr/share/puppet/modules',
+ '/usr/share/puppet/modules/crick',
+ ]: ensure => directory,
+ recurse => true,
+ purge => true,
+ force => true;
+ '/etc/puppet/modules/appleseed/metadata.json':
+ content => '{
+ "name": "jimmy/appleseed",
+ "version": "1.1.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/crakorn", "version_requirement": "0.4.0" }
+ ]
+ }';
+ '/etc/puppet/modules/thelock/metadata.json':
+ content => '{
+ "name": "jimmy/thelock",
+ "version": "1.0.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/appleseed", "version_requirement": "1.x" },
+ { "name": "jimmy/sprinkles", "version_requirement": "2.x" }
+ ]
+ }';
+ '/usr/share/puppet/modules/crick/metadata.json':
+ content => '{
+ "name": "jimmy/crick",
+ "version": "1.0.1",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/crakorn", "version_requirement": "0.4.x" }
+ ]
+ }';
+}
+PP
+on master, '[ -d /etc/puppet/modules/appleseed ]'
+on master, '[ -d /etc/puppet/modules/thelock ]'
+on master, '[ -d /usr/share/puppet/modules/crick ]'
+
+step "List the installed modules"
+on master, puppet('module list') do
+ assert_equal <<-STDERR, stderr
+\e[1;31mWarning: Missing dependency 'jimmy-crakorn':
+ 'jimmy-appleseed' (v1.1.0) requires 'jimmy-crakorn' (v0.4.0)
+ 'jimmy-crick' (v1.0.1) requires 'jimmy-crakorn' (v0.4.x)\e[0m
+\e[1;31mWarning: Missing dependency 'jimmy-sprinkles':
+ 'jimmy-thelock' (v1.0.0) requires 'jimmy-sprinkles' (v2.x)\e[0m
+STDERR
+ assert_equal <<-STDOUT, stdout
+/etc/puppet/modules
+├── jimmy-appleseed (\e[0;36mv1.1.0\e[0m)
+└── jimmy-thelock (\e[0;36mv1.0.0\e[0m)
+/usr/share/puppet/modules
+└── jimmy-crick (\e[0;36mv1.0.1\e[0m)
+STDOUT
+end
+
+step "List the installed modules as a dependency tree"
+on master, puppet('module list --tree') do
+ assert_equal <<-STDERR, stderr
+\e[1;31mWarning: Missing dependency 'jimmy-crakorn':
+ 'jimmy-appleseed' (v1.1.0) requires 'jimmy-crakorn' (v0.4.0)
+ 'jimmy-crick' (v1.0.1) requires 'jimmy-crakorn' (v0.4.x)\e[0m
+\e[1;31mWarning: Missing dependency 'jimmy-sprinkles':
+ 'jimmy-thelock' (v1.0.0) requires 'jimmy-sprinkles' (v2.x)\e[0m
+STDERR
+ assert_equal <<-STDOUT, stdout
+/etc/puppet/modules
+└─┬ jimmy-thelock (\e[0;36mv1.0.0\e[0m)
+ ├── \e[0;41mUNMET DEPENDENCY\e[0m jimmy-sprinkles (\e[0;36mv2.x\e[0m)
+ └─┬ jimmy-appleseed (\e[0;36mv1.1.0\e[0m)
+ └── \e[0;41mUNMET DEPENDENCY\e[0m jimmy-crakorn (\e[0;36mv0.4.0\e[0m)
+/usr/share/puppet/modules
+└─┬ jimmy-crick (\e[0;36mv1.0.1\e[0m)
+ └── \e[0;41mUNMET DEPENDENCY\e[0m jimmy-crakorn (\e[0;36mv0.4.x\e[0m)
+STDOUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/list/with_repeated_dependencies.rb b/acceptance/tests/modules/list/with_repeated_dependencies.rb
new file mode 100644
index 000000000..50dc8b78d
--- /dev/null
+++ b/acceptance/tests/modules/list/with_repeated_dependencies.rb
@@ -0,0 +1,113 @@
+begin test_name "puppet module list (with repeated dependencies)"
+
+step "Setup"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules',
+ '/etc/puppet/modules/crakorn',
+ '/etc/puppet/modules/steward',
+ '/etc/puppet/modules/appleseed',
+ '/etc/puppet/modules/thelock',
+ '/usr/share/puppet',
+ '/usr/share/puppet/modules',
+ '/usr/share/puppet/modules/crick',
+ ]: ensure => directory,
+ recurse => true,
+ purge => true,
+ force => true;
+ '/etc/puppet/modules/crakorn/metadata.json':
+ content => '{
+ "name": "jimmy/crakorn",
+ "version": "0.4.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/steward", "version_requirement": ">= 0.0.0" }
+ ]
+ }';
+ '/etc/puppet/modules/steward/metadata.json':
+ content => '{
+ "name": "jimmy/steward",
+ "version": "0.9.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": []
+ }';
+ '/etc/puppet/modules/appleseed/metadata.json':
+ content => '{
+ "name": "jimmy/appleseed",
+ "version": "1.1.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/crakorn", "version_requirement": "0.4.0" }
+ ]
+ }';
+ '/etc/puppet/modules/thelock/metadata.json':
+ content => '{
+ "name": "jimmy/thelock",
+ "version": "1.0.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/crakorn", "version_requirement": ">= 0.0.0" },
+ { "name": "jimmy/appleseed", "version_requirement": "1.x" }
+ ]
+ }';
+ '/usr/share/puppet/modules/crick/metadata.json':
+ content => '{
+ "name": "jimmy/crick",
+ "version": "1.0.1",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/crakorn", "version_requirement": "0.4.x" }
+ ]
+ }';
+}
+PP
+on master, '[ -d /etc/puppet/modules/crakorn ]'
+on master, '[ -d /etc/puppet/modules/steward ]'
+on master, '[ -d /etc/puppet/modules/appleseed ]'
+on master, '[ -d /etc/puppet/modules/thelock ]'
+on master, '[ -d /usr/share/puppet/modules/crick ]'
+
+step "List the installed modules"
+on master, puppet('module list') do
+ assert_equal '', stderr
+ assert_equal <<-STDOUT, stdout
+/etc/puppet/modules
+├── jimmy-appleseed (\e[0;36mv1.1.0\e[0m)
+├── jimmy-crakorn (\e[0;36mv0.4.0\e[0m)
+├── jimmy-steward (\e[0;36mv0.9.0\e[0m)
+└── jimmy-thelock (\e[0;36mv1.0.0\e[0m)
+/usr/share/puppet/modules
+└── jimmy-crick (\e[0;36mv1.0.1\e[0m)
+STDOUT
+end
+
+step "List the installed modules as a dependency tree"
+on master, puppet('module list --tree') do
+ assert_equal '', stderr
+ assert_equal <<-STDOUT, stdout
+/etc/puppet/modules
+└─┬ jimmy-thelock (\e[0;36mv1.0.0\e[0m)
+ ├─┬ jimmy-crakorn (\e[0;36mv0.4.0\e[0m)
+ │ └── jimmy-steward (\e[0;36mv0.9.0\e[0m)
+ └── jimmy-appleseed (\e[0;36mv1.1.0\e[0m)
+/usr/share/puppet/modules
+└─┬ jimmy-crick (\e[0;36mv1.0.1\e[0m)
+ └─┬ jimmy-crakorn (\e[0;36mv0.4.0\e[0m) [/etc/puppet/modules]
+ └── jimmy-steward (\e[0;36mv0.9.0\e[0m) [/etc/puppet/modules]
+STDOUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/list/without_installed_modules.rb b/acceptance/tests/modules/list/without_installed_modules.rb
new file mode 100644
index 000000000..6ec3ed33b
--- /dev/null
+++ b/acceptance/tests/modules/list/without_installed_modules.rb
@@ -0,0 +1,36 @@
+begin test_name "puppet module list (without installed modules)"
+
+step "Setup"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules',
+ '/usr/share/puppet/modules',
+ ]: ensure => directory,
+ recurse => true,
+ purge => true,
+ force => true;
+}
+PP
+
+step "List the installed modules"
+on master, puppet('module list') do
+ assert_equal '', stderr
+ assert_equal <<-STDOUT, stdout
+/etc/puppet/modules (no modules installed)
+/usr/share/puppet/modules (no modules installed)
+STDOUT
+end
+
+step "List the installed modules as a dependency tree"
+on master, puppet('module list') do
+ assert_equal '', stderr
+ assert_equal <<-STDOUT, stdout
+/etc/puppet/modules (no modules installed)
+/usr/share/puppet/modules (no modules installed)
+STDOUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/search/by_description.rb b/acceptance/tests/modules/search/by_description.rb
new file mode 100644
index 000000000..a1acfb8a4
--- /dev/null
+++ b/acceptance/tests/modules/search/by_description.rb
@@ -0,0 +1,28 @@
+begin test_name 'puppet module search should do substring matches on description'
+
+step 'Stub http://forge.puppetlabs.com'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+
+step 'Search for a module by description'
+on master, puppet("module search dummy") do
+ assert_equal '', stderr
+ # FIXME: The Forge does not presently match against description.
+# assert_equal <<-STDOUT, stdout
+# Searching http://forge.puppetlabs.com ...
+# NAME DESCRIPTION AUTHOR KEYWORDS
+# pmtacceptance-nginx This is a dummy nginx mo... @pmtacceptance nginx
+# pmtacceptance-thin This is a dummy thin mod... @pmtacceptance ruby thin
+# pmtacceptance-apollo This is a dummy apollo m... @pmtacceptance stomp apollo
+# pmtacceptance-java This is a dummy java mod... @pmtacceptance java
+# pmtacceptance-stdlib This is a dummy stdlib m... @pmtacceptance stdlib libs
+# pmtacceptance-git This is a dummy git modu... @pmtacceptance git dvcs
+# pmtacceptance-apache This is a dummy apache m... @pmtacceptance apache php
+# pmtacceptance-php This is a dummy php modu... @pmtacceptance apache php
+# pmtacceptance-geordi This is a module that do... @pmtacceptance star trek
+# STDOUT
+end
+
+ensure step 'Unstub http://forge.puppetlabs.com'
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+end
diff --git a/acceptance/tests/modules/search/by_keyword.rb b/acceptance/tests/modules/search/by_keyword.rb
new file mode 100644
index 000000000..93323d8d4
--- /dev/null
+++ b/acceptance/tests/modules/search/by_keyword.rb
@@ -0,0 +1,29 @@
+begin test_name 'puppet module search should do exact keyword matches'
+
+step 'Stub http://forge.puppetlabs.com'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+
+step 'Search for a module by exact keyword'
+on master, puppet("module search github") do
+ assert_equal '', stderr
+ assert_equal <<-STDOUT, stdout
+Searching http://forge.puppetlabs.com ...
+NAME DESCRIPTION AUTHOR KEYWORDS
+pmtacceptance-git This is a dummy git module... @pmtacceptance git \e[0;32mgithub\e[0m
+STDOUT
+end
+
+# FIXME: The Forge presently matches partial keywords.
+# step 'Search for a module by partial keyword'
+# on master, puppet("module search hub") do
+# assert_equal '', stderr
+# assert_equal <<-STDOUT, stdout
+# Searching http://forge.puppetlabs.com ...
+# No results found for 'hub'.
+# STDOUT
+# end
+
+ensure step 'Unstub http://forge.puppetlabs.com'
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+end
diff --git a/acceptance/tests/modules/search/by_module_name.rb b/acceptance/tests/modules/search/by_module_name.rb
new file mode 100644
index 000000000..be528e99f
--- /dev/null
+++ b/acceptance/tests/modules/search/by_module_name.rb
@@ -0,0 +1,40 @@
+begin test_name 'puppet module search should do substring matches on module name'
+
+step 'Stub http://forge.puppetlabs.com'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+
+step 'Search for modules by partial name'
+on master, puppet("module search geordi") do
+ assert_equal '', stderr
+ assert_equal <<-STDOUT, stdout
+Searching http://forge.puppetlabs.com ...
+NAME DESCRIPTION AUTHOR KEYWORDS
+pmtacceptance-\e[0;32mgeordi\e[0m This is a module that do... @pmtacceptance star trek
+STDOUT
+end
+
+# FIXME: The Forge does not presently support matches by dashed full name.
+# step 'Search for modules by partial full name (dashed)'
+# on master, puppet("module search tance-ge") do
+# assert_equal '', stderr
+# assert_equal <<-STDOUT, stdout
+# Searching http://forge.puppetlabs.com ...
+# NAME DESCRIPTION AUTHOR KEYWORDS
+# pmtacceptance-geordi This is a module that do... @pmtacceptance star trek
+# STDOUT
+# end
+
+step 'Search for modules by partial full name (slashed)'
+on master, puppet("module search tance/ge") do
+ assert_equal '', stderr
+ assert_equal <<-STDOUT, stdout
+Searching http://forge.puppetlabs.com ...
+NAME DESCRIPTION AUTHOR KEYWORDS
+pmtaccep\e[0;32mtance-ge\e[0mordi This is a module that do... @pmtacceptance star trek
+STDOUT
+end
+
+ensure step 'Unstub http://forge.puppetlabs.com'
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+end
diff --git a/acceptance/tests/modules/search/communication_error.rb b/acceptance/tests/modules/search/communication_error.rb
new file mode 100644
index 000000000..8fb396e26
--- /dev/null
+++ b/acceptance/tests/modules/search/communication_error.rb
@@ -0,0 +1,20 @@
+begin test_name 'puppet module search should print a reasonable message on communication errors'
+
+step 'Stub http://forge.puppetlabs.com'
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '127.0.0.2' }"
+
+step "Search against a non-existent Forge"
+on master, puppet("module search yup"), :acceptable_exit_codes => [1] do
+ assert_equal <<-STDOUT, stdout
+Searching http://forge.puppetlabs.com ...
+STDOUT
+ assert_equal <<-STDERR, stderr
+Error: Could not connect to http://forge.puppetlabs.com
+ There was a network communications problem
+ Check your network connection and try again
+STDERR
+end
+
+ensure step 'Unstub http://forge.puppetlabs.com'
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+end
diff --git a/acceptance/tests/modules/search/formatting.rb b/acceptance/tests/modules/search/formatting.rb
new file mode 100644
index 000000000..c196af9b0
--- /dev/null
+++ b/acceptance/tests/modules/search/formatting.rb
@@ -0,0 +1,22 @@
+begin test_name 'puppet module search output should be well structured'
+
+step 'Stub http://forge.puppetlabs.com'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+
+step 'Search results should line up by column'
+on master, puppet("module search apache") do
+ assert_equal('', stderr)
+
+ assert_equal "Searching http://forge.puppetlabs.com ...\n", stdout.lines.first
+ columns = stdout.lines.to_a[1].split(/\s{2}(?=\S)/)
+ pattern = /^#{ columns.map { |c| c.chomp.gsub(/./, '.') }.join(' ') }$/
+
+ stdout.gsub(/\e.*?m/, '').lines.to_a[1..-1].each do |line|
+ assert_match(pattern, line.chomp, 'columns were misaligned')
+ end
+end
+
+ensure step 'Unstub http://forge.puppetlabs.com'
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+end
diff --git a/acceptance/tests/modules/search/multiple_search_terms.rb b/acceptance/tests/modules/search/multiple_search_terms.rb
new file mode 100644
index 000000000..7ba88554b
--- /dev/null
+++ b/acceptance/tests/modules/search/multiple_search_terms.rb
@@ -0,0 +1,25 @@
+begin test_name 'puppet module search should handle multiple search terms sensibly'
+
+step 'Stub http://forge.puppetlabs.com'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+
+# FIXME: The Forge doesn't properly handle multi-term searches.
+# step 'Search for a module by description'
+# on master, puppet("module search 'notice here'") do
+# assert stdout !~ /'notice here'/
+# end
+#
+# step 'Search for a module by name'
+# on master, puppet("module search 'ance-geo ance-std'") do
+# assert stdout !~ /'ance-geo ance-std'/
+# end
+#
+# step 'Search for multiple keywords'
+# on master, puppet("module search 'star trek'") do
+# assert stdout !~ /'star trek'/
+# end
+
+ensure step 'Unstub http://forge.puppetlabs.com'
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+end
diff --git a/acceptance/tests/modules/search/no_results.rb b/acceptance/tests/modules/search/no_results.rb
new file mode 100644
index 000000000..7d8ee887a
--- /dev/null
+++ b/acceptance/tests/modules/search/no_results.rb
@@ -0,0 +1,18 @@
+begin test_name 'puppet module search should print a reasonable message for no results'
+
+step 'Stub http://forge.puppetlabs.com'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+
+step "Search for a module that doesn't exist"
+on master, puppet("module search module_not_appearing_in_this_forge") do
+ assert_equal '', stderr
+ assert_equal <<-STDOUT, stdout
+Searching http://forge.puppetlabs.com ...
+No results found for 'module_not_appearing_in_this_forge'.
+STDOUT
+end
+
+ensure step 'Unstub http://forge.puppetlabs.com'
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+end
diff --git a/acceptance/tests/modules/uninstall/using_directory_name.rb b/acceptance/tests/modules/uninstall/using_directory_name.rb
new file mode 100644
index 000000000..63e94e0c1
--- /dev/null
+++ b/acceptance/tests/modules/uninstall/using_directory_name.rb
@@ -0,0 +1,47 @@
+begin test_name "puppet module uninstall (using directory name)"
+
+step "Setup"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules',
+ '/etc/puppet/modules/apache',
+ '/etc/puppet/modules/crakorn',
+ ]: ensure => directory;
+ '/etc/puppet/modules/crakorn/metadata.json':
+ content => '{
+ "name": "jimmy/crakorn",
+ "version": "0.4.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": []
+ }';
+}
+PP
+on master, '[ -d /etc/puppet/modules/apache ]'
+on master, '[ -d /etc/puppet/modules/crakorn ]'
+
+step "Try to uninstall the module apache"
+on master, puppet('module uninstall apache') do
+ assert_output <<-OUTPUT
+ Preparing to uninstall 'apache' ...
+ Removed 'apache' from /etc/puppet/modules
+ OUTPUT
+end
+on master, '[ ! -d /etc/puppet/modules/apache ]'
+
+step "Try to uninstall the module crakorn"
+on master, puppet('module uninstall crakorn'), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to uninstall 'crakorn' ...
+ STDERR> \e[1;31mError: Could not uninstall module 'crakorn'
+ STDERR> Module 'crakorn' is not installed
+ STDERR> You may have meant `puppet module uninstall jimmy-crakorn`\e[0m
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/crakorn ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/uninstall/using_version_filter.rb b/acceptance/tests/modules/uninstall/using_version_filter.rb
new file mode 100644
index 000000000..b779556f5
--- /dev/null
+++ b/acceptance/tests/modules/uninstall/using_version_filter.rb
@@ -0,0 +1,59 @@
+begin test_name "puppet module uninstall (with module installed)"
+
+step "Setup"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules',
+ '/etc/puppet/modules/crakorn',
+ '/usr/share/puppet',
+ '/usr/share/puppet/modules',
+ '/usr/share/puppet/modules/crakorn',
+ ]: ensure => directory;
+ '/etc/puppet/modules/crakorn/metadata.json':
+ content => '{
+ "name": "jimmy/crakorn",
+ "version": "0.4.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": []
+ }';
+ '/usr/share/puppet/modules/crakorn/metadata.json':
+ content => '{
+ "name": "jimmy/crakorn",
+ "version": "0.5.1",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": []
+ }';
+}
+PP
+on master, '[ -d /etc/puppet/modules/crakorn ]'
+on master, '[ -d /usr/share/puppet/modules/crakorn ]'
+
+step "Uninstall jimmy-crakorn version 0.5.x"
+on master, puppet('module uninstall jimmy-crakorn --version 0.5.x') do
+ assert_output <<-OUTPUT
+ Preparing to uninstall 'jimmy-crakorn' (\e[0;36mv0.5.x\e[0m) ...
+ Removed 'jimmy-crakorn' (\e[0;36mv0.5.1\e[0m) from /usr/share/puppet/modules
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/crakorn ]'
+on master, '[ ! -d /usr/share/puppet/modules/crakorn ]'
+
+step "Try to uninstall jimmy-crakorn v0.4.0 with `--version 0.5.x`"
+on master, puppet('module uninstall jimmy-crakorn --version 0.5.x'), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to uninstall 'jimmy-crakorn' (\e[0;36mv0.5.x\e[0m) ...
+ STDERR> \e[1;31mError: Could not uninstall module 'jimmy-crakorn' (v0.5.x)
+ STDERR> No installed version of 'jimmy-crakorn' matches (v0.5.x)
+ STDERR> 'jimmy-crakorn' (v0.4.0) is installed in /etc/puppet/modules\e[0m
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/crakorn ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/uninstall/with_active_dependency.rb b/acceptance/tests/modules/uninstall/with_active_dependency.rb
new file mode 100644
index 000000000..1d7e42c20
--- /dev/null
+++ b/acceptance/tests/modules/uninstall/with_active_dependency.rb
@@ -0,0 +1,74 @@
+begin test_name "puppet module uninstall (with active dependency)"
+
+step "Setup"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules',
+ '/etc/puppet/modules/crakorn',
+ '/etc/puppet/modules/appleseed',
+ ]: ensure => directory;
+ '/etc/puppet/modules/crakorn/metadata.json':
+ content => '{
+ "name": "jimmy/crakorn",
+ "version": "0.4.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": []
+ }';
+ '/etc/puppet/modules/appleseed/metadata.json':
+ content => '{
+ "name": "jimmy/appleseed",
+ "version": "1.1.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/crakorn", "version_requirement": "0.4.0" }
+ ]
+ }';
+}
+PP
+on master, '[ -d /etc/puppet/modules/crakorn ]'
+on master, '[ -d /etc/puppet/modules/appleseed ]'
+
+step "Try to uninstall the module jimmy-crakorn"
+on master, puppet('module uninstall jimmy-crakorn'), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to uninstall 'jimmy-crakorn' ...
+ STDERR> \e[1;31mError: Could not uninstall module 'jimmy-crakorn'
+ STDERR> Other installed modules have dependencies on 'jimmy-crakorn' (v0.4.0)
+ STDERR> 'jimmy/appleseed' (v1.1.0) requires 'jimmy-crakorn' (v0.4.0)
+ STDERR> Use `puppet module uninstall --force` to uninstall this module anyway\e[0m
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/crakorn ]'
+on master, '[ -d /etc/puppet/modules/appleseed ]'
+
+step "Try to uninstall the module jimmy-crakorn with a version range"
+on master, puppet('module uninstall jimmy-crakorn --version 0.x'), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to uninstall 'jimmy-crakorn' (\e[0;36mv0.x\e[0m) ...
+ STDERR> \e[1;31mError: Could not uninstall module 'jimmy-crakorn' (v0.x)
+ STDERR> Other installed modules have dependencies on 'jimmy-crakorn' (v0.4.0)
+ STDERR> 'jimmy/appleseed' (v1.1.0) requires 'jimmy-crakorn' (v0.4.0)
+ STDERR> Use `puppet module uninstall --force` to uninstall this module anyway\e[0m
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/crakorn ]'
+on master, '[ -d /etc/puppet/modules/appleseed ]'
+
+step "Uninstall the module jimmy-crakorn forcefully"
+on master, puppet('module uninstall jimmy-crakorn --force') do
+ assert_output <<-OUTPUT
+ Preparing to uninstall 'jimmy-crakorn' ...
+ Removed 'jimmy-crakorn' (\e[0;36mv0.4.0\e[0m) from /etc/puppet/modules
+ OUTPUT
+end
+on master, '[ ! -d /etc/puppet/modules/crakorn ]'
+on master, '[ -d /etc/puppet/modules/appleseed ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/uninstall/with_module_installed.rb b/acceptance/tests/modules/uninstall/with_module_installed.rb
new file mode 100644
index 000000000..374e5225e
--- /dev/null
+++ b/acceptance/tests/modules/uninstall/with_module_installed.rb
@@ -0,0 +1,34 @@
+begin test_name "puppet module uninstall (with module installed)"
+
+step "Setup"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules',
+ '/etc/puppet/modules/crakorn',
+ ]: ensure => directory;
+ '/etc/puppet/modules/crakorn/metadata.json':
+ content => '{
+ "name": "jimmy/crakorn",
+ "version": "0.4.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": []
+ }';
+}
+PP
+on master, '[ -d /etc/puppet/modules/crakorn ]'
+
+step "Uninstall the module jimmy-crakorn"
+on master, puppet('module uninstall jimmy-crakorn') do
+ assert_output <<-OUTPUT
+ Preparing to uninstall 'jimmy-crakorn' ...
+ Removed 'jimmy-crakorn' (\e[0;36mv0.4.0\e[0m) from /etc/puppet/modules
+ OUTPUT
+end
+on master, '[ ! -d /etc/puppet/modules/crakorn ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/uninstall/with_multiple_modules_installed.rb b/acceptance/tests/modules/uninstall/with_multiple_modules_installed.rb
new file mode 100644
index 000000000..37309aa01
--- /dev/null
+++ b/acceptance/tests/modules/uninstall/with_multiple_modules_installed.rb
@@ -0,0 +1,43 @@
+begin test_name "puppet module uninstall (with multiple modules installed)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+on master, puppet("module install pmtacceptance-java --version 1.6.0 --modulepath /etc/puppet/modules")
+on master, puppet("module install pmtacceptance-java --version 1.7.0 --modulepath /usr/share/puppet/modules")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.7.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ OUTPUT
+end
+
+step "Try to uninstall a module that exists multiple locations in the module path"
+on master, puppet("module uninstall pmtacceptance-java"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to uninstall 'pmtacceptance-java' ...
+ STDERR> \e[1;31mError: Could not uninstall module 'pmtacceptance-java'
+ STDERR> Module 'pmtacceptance-java' appears multiple places in the module path
+ STDERR> 'pmtacceptance-java' (v1.6.0) was found in /etc/puppet/modules
+ STDERR> 'pmtacceptance-java' (v1.7.0) was found in /usr/share/puppet/modules
+ STDERR> Use the `--modulepath` option to limit the search to specific directories\e[0m
+ OUTPUT
+end
+
+step "Uninstall a module that exists multiple locations by restricting the --modulepath"
+on master, puppet("module uninstall pmtacceptance-java --modulepath /etc/puppet/modules") do
+ assert_output <<-OUTPUT
+ Preparing to uninstall 'pmtacceptance-java' ...
+ Removed 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) from /etc/puppet/modules
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/upgrade/in_a_secondary_directory.rb b/acceptance/tests/modules/upgrade/in_a_secondary_directory.rb
new file mode 100644
index 000000000..d7b9d4df8
--- /dev/null
+++ b/acceptance/tests/modules/upgrade/in_a_secondary_directory.rb
@@ -0,0 +1,32 @@
+begin test_name "puppet module upgrade (in a secondary directory)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+on master, puppet("module install pmtacceptance-java --version 1.6.0 --target-dir /usr/share/puppet/modules")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules (no modules installed)
+ /usr/share/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ OUTPUT
+end
+
+step "Upgrade a module that has a more recent version published"
+on master, puppet("module upgrade pmtacceptance-java") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-java' ...
+ Found 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) in /usr/share/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /usr/share/puppet/modules
+ └── pmtacceptance-java (\e[0;36mv1.6.0 -> v1.7.1\e[0m)
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/upgrade/introducing_new_dependencies.rb b/acceptance/tests/modules/upgrade/introducing_new_dependencies.rb
new file mode 100644
index 000000000..14e924da3
--- /dev/null
+++ b/acceptance/tests/modules/upgrade/introducing_new_dependencies.rb
@@ -0,0 +1,36 @@
+begin test_name "puppet module upgrade (introducing new dependencies)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+on master, puppet("module install pmtacceptance-stdlib --version 1.0.0")
+on master, puppet("module install pmtacceptance-java --version 1.7.0")
+on master, puppet("module install pmtacceptance-postgresql --version 0.0.2")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.7.0\e[0m)
+ ├── pmtacceptance-postgresql (\e[0;36mv0.0.2\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+step "Upgrade a module to a version that introduces new dependencies"
+on master, puppet("module upgrade pmtacceptance-postgresql") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-postgresql' ...
+ Found 'pmtacceptance-postgresql' (\e[0;36mv0.0.2\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └─┬ pmtacceptance-postgresql (\e[0;36mv0.0.2 -> v1.0.0\e[0m)
+ └── pmtacceptance-geordi (\e[0;36mv0.0.1\e[0m)
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/upgrade/not_upgradable.rb b/acceptance/tests/modules/upgrade/not_upgradable.rb
new file mode 100644
index 000000000..dfb8ed5ff
--- /dev/null
+++ b/acceptance/tests/modules/upgrade/not_upgradable.rb
@@ -0,0 +1,82 @@
+begin test_name "puppet module upgrade (not upgradable)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+apply_manifest_on master, <<-PP
+ file {
+ [
+ '/etc/puppet/modules/nginx',
+ '/etc/puppet/modules/unicorns',
+ ]: ensure => directory;
+ '/etc/puppet/modules/unicorns/metadata.json':
+ content => '{
+ "name": "notpmtacceptance/unicorns",
+ "version": "0.0.3",
+ "source": "",
+ "author": "notpmtacceptance",
+ "license": "MIT",
+ "dependencies": []
+ }';
+ }
+PP
+on master, puppet("module install pmtacceptance-java --version 1.6.0")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── nginx (\e[0;36m???\e[0m)
+ ├── notpmtacceptance-unicorns (\e[0;36mv0.0.3\e[0m)
+ ├── pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+step "Try to upgrade a module that is not installed"
+on master, puppet("module upgrade pmtacceptance-nginx"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to upgrade 'pmtacceptance-nginx' ...
+ STDERR> \e[1;31mError: Could not upgrade module 'pmtacceptance-nginx'
+ STDERR> Module 'pmtacceptance-nginx' is not installed
+ STDERR> Use `puppet module install` to install this module\e[0m
+ OUTPUT
+end
+
+step "Try to upgrade a local module"
+on master, puppet("module upgrade nginx"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to upgrade 'nginx' ...
+ STDOUT> Found 'nginx' (\e[0;36m???\e[0m) in /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not upgrade module 'nginx' (??? -> latest)
+ STDERR> Module 'nginx' does not exist on http://forge.puppetlabs.com\e[0m
+ OUTPUT
+end
+
+step "Try to upgrade a module that doesn't exist"
+on master, puppet("module upgrade notpmtacceptance-unicorns"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to upgrade 'notpmtacceptance-unicorns' ...
+ STDOUT> Found 'notpmtacceptance-unicorns' (\e[0;36mv0.0.3\e[0m) in /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not upgrade module 'notpmtacceptance-unicorns' (v0.0.3 -> latest)
+ STDERR> Module 'notpmtacceptance-unicorns' does not exist on http://forge.puppetlabs.com\e[0m
+ OUTPUT
+end
+
+step "Try to upgrade an installed module to a version that doesn't exist"
+on master, puppet("module upgrade pmtacceptance-java --version 2.0.0"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to upgrade 'pmtacceptance-java' ...
+ STDOUT> Found 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) in /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not upgrade module 'pmtacceptance-java' (v1.6.0 -> v2.0.0)
+ STDERR> No version matching '2.0.0' exists on http://forge.puppetlabs.com\e[0m
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/upgrade/that_was_installed_twice.rb b/acceptance/tests/modules/upgrade/that_was_installed_twice.rb
new file mode 100644
index 000000000..c42952634
--- /dev/null
+++ b/acceptance/tests/modules/upgrade/that_was_installed_twice.rb
@@ -0,0 +1,47 @@
+begin test_name "puppet module upgrade (that was installed twice)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+on master, puppet("module install pmtacceptance-java --version 1.6.0 --modulepath /etc/puppet/modules")
+on master, puppet("module install pmtacceptance-java --version 1.7.0 --modulepath /usr/share/puppet/modules")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.7.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ OUTPUT
+end
+
+step "Try to upgrade a module that exists multiple locations in the module path"
+on master, puppet("module upgrade pmtacceptance-java"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to upgrade 'pmtacceptance-java' ...
+ STDERR> \e[1;31mError: Could not upgrade module 'pmtacceptance-java'
+ STDERR> Module 'pmtacceptance-java' appears multiple places in the module path
+ STDERR> 'pmtacceptance-java' (v1.6.0) was found in /etc/puppet/modules
+ STDERR> 'pmtacceptance-java' (v1.7.0) was found in /usr/share/puppet/modules
+ STDERR> Use the `--modulepath` option to limit the search to specific directories\e[0m
+ OUTPUT
+end
+
+step "Upgrade a module that exists multiple locations by restricting the --modulepath"
+on master, puppet("module upgrade pmtacceptance-java --modulepath /etc/puppet/modules") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-java' ...
+ Found 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-java (\e[0;36mv1.6.0 -> v1.7.1\e[0m)
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/upgrade/to_a_specific_version.rb b/acceptance/tests/modules/upgrade/to_a_specific_version.rb
new file mode 100644
index 000000000..77889d7d3
--- /dev/null
+++ b/acceptance/tests/modules/upgrade/to_a_specific_version.rb
@@ -0,0 +1,44 @@
+begin test_name "puppet module upgrade (to a specific version)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+on master, puppet("module install pmtacceptance-java --version 1.6.0")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+step "Upgrade a module to a specific (greater) version"
+on master, puppet("module upgrade pmtacceptance-java --version 1.7.0") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-java' ...
+ Found 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-java (\e[0;36mv1.6.0 -> v1.7.0\e[0m)
+ OUTPUT
+end
+
+step "Upgrade a module to a specific (lesser) version"
+on master, puppet("module upgrade pmtacceptance-java --version 1.6.0") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-java' ...
+ Found 'pmtacceptance-java' (\e[0;36mv1.7.0\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-java (\e[0;36mv1.7.0 -> v1.6.0\e[0m)
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/upgrade/to_installed_version.rb b/acceptance/tests/modules/upgrade/to_installed_version.rb
new file mode 100644
index 000000000..496749ba1
--- /dev/null
+++ b/acceptance/tests/modules/upgrade/to_installed_version.rb
@@ -0,0 +1,81 @@
+begin test_name "puppet module upgrade (to installed version)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+on master, puppet("module install pmtacceptance-java --version 1.6.0")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+step "Try to upgrade a module to the current version"
+on master, puppet("module upgrade pmtacceptance-java --version 1.6.x"), :acceptable_exit_codes => [0] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to upgrade 'pmtacceptance-java' ...
+ STDOUT> Found 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) in /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not upgrade module 'pmtacceptance-java' (v1.6.0 -> v1.6.x)
+ STDERR> The installed version is already the best fit for the current dependencies
+ STDERR> You specified 'pmtacceptance-java' (v1.6.x)
+ STDERR> Use `puppet module install --force` to re-install this module\e[0m
+ OUTPUT
+end
+
+step "Upgrade a module to the current version with --force"
+on master, puppet("module upgrade pmtacceptance-java --version 1.6.x --force") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-java' ...
+ Found 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-java (\e[0;36mv1.6.0 -> v1.6.0\e[0m)
+ OUTPUT
+end
+
+step "Upgrade to the latest version"
+on master, puppet("module upgrade pmtacceptance-java") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-java' ...
+ Found 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-java (\e[0;36mv1.6.0 -> v1.7.1\e[0m)
+ OUTPUT
+end
+
+step "Try to upgrade a module to the latest version with the latest version installed"
+on master, puppet("module upgrade pmtacceptance-java"), :acceptable_exit_codes => [0] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to upgrade 'pmtacceptance-java' ...
+ STDOUT> Found 'pmtacceptance-java' (\e[0;36mv1.7.1\e[0m) in /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not upgrade module 'pmtacceptance-java' (v1.7.1 -> latest: v1.7.1)
+ STDERR> The installed version is already the latest version
+ STDERR> Use `puppet module install --force` to re-install this module\e[0m
+ OUTPUT
+end
+
+step "Upgrade a module to the latest version with --force"
+on master, puppet("module upgrade pmtacceptance-java --force") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-java' ...
+ Found 'pmtacceptance-java' (\e[0;36mv1.7.1\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-java (\e[0;36mv1.7.1 -> v1.7.1\e[0m)
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/upgrade/with_constraints_on_it.rb b/acceptance/tests/modules/upgrade/with_constraints_on_it.rb
new file mode 100644
index 000000000..0a5238acc
--- /dev/null
+++ b/acceptance/tests/modules/upgrade/with_constraints_on_it.rb
@@ -0,0 +1,48 @@
+begin test_name "puppet module upgrade (with constraints on it)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+on master, puppet("module install pmtacceptance-java --version 1.7.0")
+on master, puppet("module install pmtacceptance-apollo")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-apollo (\e[0;36mv0.0.1\e[0m)
+ ├── pmtacceptance-java (\e[0;36mv1.7.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+step "Upgrade a version-constrained module that has an upgrade"
+on master, puppet("module upgrade pmtacceptance-java") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-java' ...
+ Found 'pmtacceptance-java' (\e[0;36mv1.7.0\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-java (\e[0;36mv1.7.0 -> v1.7.1\e[0m)
+ OUTPUT
+end
+
+step "Try to upgrade a version-constrained module that has no upgrade"
+on master, puppet("module upgrade pmtacceptance-stdlib"), :acceptable_exit_codes => [0] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to upgrade 'pmtacceptance-stdlib' ...
+ STDOUT> Found 'pmtacceptance-stdlib' (\e[0;36mv1.0.0\e[0m) in /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not upgrade module 'pmtacceptance-stdlib' (v1.0.0 -> best: v1.0.0)
+ STDERR> The installed version is already the best fit for the current dependencies
+ STDERR> 'pmtacceptance-apollo' (v0.0.1) requires 'pmtacceptance-stdlib' (>= 1.0.0)
+ STDERR> 'pmtacceptance-java' (v1.7.1) requires 'pmtacceptance-stdlib' (v1.0.0)
+ STDERR> Use `puppet module install --force` to re-install this module\e[0m
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/upgrade/with_constraints_on_its_dependencies.rb b/acceptance/tests/modules/upgrade/with_constraints_on_its_dependencies.rb
new file mode 100644
index 000000000..03f131d8c
--- /dev/null
+++ b/acceptance/tests/modules/upgrade/with_constraints_on_its_dependencies.rb
@@ -0,0 +1,90 @@
+begin test_name "puppet module upgrade (with constraints on its dependencies)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+apply_manifest_on master, <<-PP
+ file {
+ [
+ '/etc/puppet/modules/unicorns',
+ ]: ensure => directory;
+ '/etc/puppet/modules/unicorns/metadata.json':
+ content => '{
+ "name": "notpmtacceptance/unicorns",
+ "version": "0.0.3",
+ "source": "",
+ "author": "notpmtacceptance",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "pmtacceptance/stdlib", "version_requirement": "0.0.2" }
+ ]
+ }';
+ }
+PP
+on master, puppet("module install pmtacceptance-stdlib --version 0.0.2")
+on master, puppet("module install pmtacceptance-java --version 1.6.0")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── notpmtacceptance-unicorns (\e[0;36mv0.0.3\e[0m)
+ ├── pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv0.0.2\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+step "Try to upgrade a module with constraints on its dependencies that cannot be met"
+on master, puppet("module upgrade pmtacceptance-java"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to upgrade 'pmtacceptance-java' ...
+ STDOUT> Found 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) in /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not upgrade module 'pmtacceptance-java' (v1.6.0 -> latest: v1.7.1)
+ STDERR> No version of 'pmtacceptance-stdlib' will satisfy dependencies
+ STDERR> 'notpmtacceptance-unicorns' (v0.0.3) requires 'pmtacceptance-stdlib' (v0.0.2)
+ STDERR> 'pmtacceptance-java' (v1.7.1) requires 'pmtacceptance-stdlib' (v1.0.0)
+ STDERR> Use `puppet module upgrade --ignore-dependencies` to upgrade only this module\e[0m
+ OUTPUT
+end
+
+step "Relax constraints"
+on master, puppet("module uninstall notpmtacceptance-unicorns")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv0.0.2\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+step "Upgrade a single module, ignoring its dependencies"
+on master, puppet("module upgrade pmtacceptance-java --version 1.7.0 --ignore-dependencies") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-java' ...
+ Found 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-java (\e[0;36mv1.6.0 -> v1.7.0\e[0m)
+ OUTPUT
+end
+
+step "Upgrade a module with constraints on its dependencies that can be met"
+on master, puppet("module upgrade pmtacceptance-java") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-java' ...
+ Found 'pmtacceptance-java' (\e[0;36mv1.7.0\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └─┬ pmtacceptance-java (\e[0;36mv1.7.0 -> v1.7.1\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv0.0.2 -> v1.0.0\e[0m)
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/upgrade/with_local_changes.rb b/acceptance/tests/modules/upgrade/with_local_changes.rb
new file mode 100644
index 000000000..3d75a317d
--- /dev/null
+++ b/acceptance/tests/modules/upgrade/with_local_changes.rb
@@ -0,0 +1,53 @@
+begin test_name "puppet module upgrade (with local changes)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+on master, puppet("module install pmtacceptance-java --version 1.6.0")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+apply_manifest_on master, <<-PP
+ file {
+ '/etc/puppet/modules/java/README': content => "I CHANGE MY READMES";
+ '/etc/puppet/modules/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
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to upgrade 'pmtacceptance-java' ...
+ STDOUT> Found 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) in /etc/puppet/modules ...
+ STDERR> \e[1;31mError: Could not upgrade module 'pmtacceptance-java' (v1.6.0 -> latest)
+ STDERR> Installed module has had changes made locally
+ STDERR> Use `puppet module upgrade --force` to upgrade this module anyway\e[0m
+ OUTPUT
+end
+on master, '[[ "$(cat /etc/puppet/modules/java/README)" == "I CHANGE MY READMES" ]]'
+on master, '[ -f /etc/puppet/modules/java/NEWFILE ]'
+
+step "Upgrade a module with local changes with --force"
+on master, puppet("module upgrade pmtacceptance-java --force") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-java' ...
+ Found 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-java (\e[0;36mv1.6.0 -> v1.7.1\e[0m)
+ OUTPUT
+end
+on master, '[[ "$(cat /etc/puppet/modules/java/README)" != "I CHANGE MY READMES" ]]'
+on master, '[ ! -f /etc/puppet/modules/java/NEWFILE ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/upgrade/with_scattered_dependencies.rb b/acceptance/tests/modules/upgrade/with_scattered_dependencies.rb
new file mode 100644
index 000000000..53612d217
--- /dev/null
+++ b/acceptance/tests/modules/upgrade/with_scattered_dependencies.rb
@@ -0,0 +1,38 @@
+begin test_name "puppet module upgrade (with scattered dependencies)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+on master, puppet("module install pmtacceptance-stdlib --version 0.0.2 --target-dir /usr/share/puppet/modules")
+on master, puppet("module install pmtacceptance-java --version 1.6.0 --target-dir /etc/puppet/modules --ignore-dependencies")
+on master, puppet("module install pmtacceptance-postgresql --version 0.0.1 --target-dir /etc/puppet/modules --ignore-dependencies")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-postgresql (\e[0;36mv0.0.1\e[0m)
+ /usr/share/puppet/modules
+ └── pmtacceptance-stdlib (\e[0;36mv0.0.2\e[0m)
+ OUTPUT
+end
+
+step "Upgrade a module that has a more recent version published"
+on master, puppet("module upgrade pmtacceptance-postgresql --version 0.0.2") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-postgresql' ...
+ Found 'pmtacceptance-postgresql' (\e[0;36mv0.0.1\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └─┬ pmtacceptance-postgresql (\e[0;36mv0.0.1 -> v0.0.2\e[0m)
+ ├─┬ pmtacceptance-java (\e[0;36mv1.6.0 -> v1.7.0\e[0m)
+ │ └── pmtacceptance-stdlib (\e[0;36mv0.0.2 -> v1.0.0\e[0m) [/usr/share/puppet/modules]
+ └── pmtacceptance-stdlib (\e[0;36mv0.0.2 -> v1.0.0\e[0m) [/usr/share/puppet/modules]
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/upgrade/with_update_available.rb b/acceptance/tests/modules/upgrade/with_update_available.rb
new file mode 100644
index 000000000..0109f81e2
--- /dev/null
+++ b/acceptance/tests/modules/upgrade/with_update_available.rb
@@ -0,0 +1,32 @@
+begin test_name "puppet module upgrade (with update available)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+on master, puppet("module install pmtacceptance-java --version 1.6.0")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+step "Upgrade a module that has a more recent version published"
+on master, puppet("module upgrade pmtacceptance-java") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-java' ...
+ Found 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-java (\e[0;36mv1.6.0 -> v1.7.1\e[0m)
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/lib/puppet/application/module.rb b/lib/puppet/application/module.rb
new file mode 100644
index 000000000..e10766035
--- /dev/null
+++ b/lib/puppet/application/module.rb
@@ -0,0 +1,11 @@
+require 'puppet/application/face_base'
+
+class Puppet::Application::Module < Puppet::Application::FaceBase
+ def setup
+ super
+ if self.render_as.name == :console
+ Puppet::Util::Log.close(:console)
+ Puppet::Util::Log.newdestination(:telly_prototype_console)
+ end
+ end
+end
diff --git a/lib/puppet/face/help/man.erb b/lib/puppet/face/help/man.erb
index 90e27964c..5a3aa2cbe 100644
--- a/lib/puppet/face/help/man.erb
+++ b/lib/puppet/face/help/man.erb
@@ -1,136 +1,136 @@
puppet-<%= face.name %>(8) -- <%= face.summary || "Undocumented subcommand." %>
<%= '=' * (_erbout.length - 1) %>
<% if face.synopsis -%>
SYNOPSIS
--------
<%= face.synopsis %>
<% end
if face.description -%>
DESCRIPTION
-----------
<%= face.description.strip %>
<% end -%>
OPTIONS
-------
Note that any configuration parameter that's valid in the configuration
file is also a valid long argument, although it may or may not be
relevant to the present action. For example, `server` is a valid
configuration parameter, so you can specify `--server ` as
an argument.
See the configuration file documentation at
for the
full list of acceptable parameters. A commented list of all
configuration options can also be generated by running puppet with
`--genconfig`.
* --mode MODE:
The run mode to use for the current action. Valid modes are `user`, `agent`,
and `master`.
* --render-as FORMAT:
The format in which to render output. The most common formats are `json`,
`s` (string), `yaml`, and `console`, but other options such as `dot` are
sometimes available.
* --verbose:
Whether to log verbosely.
* --debug:
Whether to log debug information.
<% unless face.options.empty?
face.options.sort.each do |name|
option = face.get_option name -%>
<%= "* " + option.optparse.join(" | " ) %>:
<%= option.description.gsub(/^/, ' ') || ' ' + option.summary %>
<% end
end -%>
ACTIONS
-------
<% face.actions.each do |actionname|
action = face.get_action(actionname) -%>
* `<%= action.name.to_s %>` - <%= action.summary %>:
<% if action.synopsis -%>
`SYNOPSIS`
<%= action.synopsis %>
<% end -%>
`DESCRIPTION`
<% if action.description -%>
<%= action.description.gsub(/^/, ' ') %>
<% else -%>
<%= action.summary || "Undocumented action." %>
<% end -%>
<% unique_options = action.options - face.options
unless unique_options.empty? -%>
`OPTIONS`
<% unique_options.sort.each do |name|
option = action.get_option name
- text = option.description || option.summary -%>
+ text = (option.description || option.summary).chomp + "\n" -%>
<%= '<' + option.optparse.join("> | <") + '>' %> -
<%= text.gsub(/^/, ' ') %>
<% end -%>
<% end -%>
<% if action.returns -%>
`RETURNS`
<%= action.returns.gsub(/^/, ' ') %>
<% end
if action.notes -%>
`NOTES`
<%= action.notes.gsub(/^/, ' ') %>
<% end
end
if face.examples or face.actions.any? {|actionname| face.get_action(actionname).examples} -%>
EXAMPLES
--------
<% end
if face.examples -%>
<%= face.examples %>
<% end
face.actions.each do |actionname|
action = face.get_action(actionname)
if action.examples -%>
`<%= action.name.to_s %>`
<%= action.examples.strip %>
<% end
end -%>
<% if face.notes or face.respond_to? :indirection -%>
NOTES
-----
<% if face.notes -%>
<%= face.notes.strip %>
<% end # notes
if face.respond_to? :indirection -%>
This subcommand is an indirector face, which exposes `find`, `search`, `save`,
and `destroy` actions for an indirected subsystem of Puppet. Valid termini for
this face include:
* `<%= face.class.terminus_classes(face.indirection.name).join("`\n* `") %>`
<% end # indirection
end # notes or indirection
unless face.authors.empty? -%>
AUTHOR
------
<%= face.authors.join("\n").gsub(/^/, ' * ') %>
<% end -%>
COPYRIGHT AND LICENSE
---------------------
<%= face.copyright %>
<%= face.license %>
diff --git a/lib/puppet/face/module.rb b/lib/puppet/face/module.rb
new file mode 100644
index 000000000..08de3cb2a
--- /dev/null
+++ b/lib/puppet/face/module.rb
@@ -0,0 +1,17 @@
+require 'puppet/face'
+require 'puppet/module_tool'
+require 'puppet/util/colors'
+
+Puppet::Face.define(:module, '1.0.0') do
+ extend Puppet::Util::Colors
+
+ copyright "Puppet Labs", 2011
+ license "Apache 2 license; see COPYING"
+
+ summary "Creates, installs and searches for modules on the Puppet Forge."
+ description <<-EOT
+ This subcommand can find, install, and manage modules from the Puppet Forge,
+ a repository of user-contributed Puppet code. It can also generate empty
+ modules, and prepare locally developed modules for release on the Forge.
+ EOT
+end
diff --git a/lib/puppet/face/module/build.rb b/lib/puppet/face/module/build.rb
index 227363448..f1a0ef466 100644
--- a/lib/puppet/face/module/build.rb
+++ b/lib/puppet/face/module/build.rb
@@ -1,31 +1,39 @@
Puppet::Face.define(:module, '1.0.0') do
action(:build) do
summary "Build a module release package."
description <<-EOT
- Build a module release archive file by processing the Modulefile in the
- module directory. The release archive file will be stored in the pkg
- directory of the module directory.
+ Prepares a local module for release on the Puppet Forge by building a
+ ready-to-upload archive file. Before using this action, make sure that
+ the module directory's name is in the standard -
+ format.
+
+ This action uses the Modulefile in the module directory to set metadata
+ used by the Forge. See for more
+ about writing modulefiles.
+
+ After being built, the release archive file can be found in the module's
+ `pkg` directory.
EOT
returns "Pathname object representing the path to the release archive."
examples <<-EOT
Build a module release:
$ puppet module build puppetlabs-apache
notice: Building /Users/kelseyhightower/puppetlabs-apache for release
puppetlabs-apache/pkg/puppetlabs-apache-0.0.1.tar.gz
EOT
arguments ""
when_invoked do |path, options|
Puppet::Module::Tool::Applications::Builder.run(path, options)
end
when_rendering :console do |return_value|
# Get the string representation of the Pathname object.
return_value.to_s
end
end
end
diff --git a/lib/puppet/face/module/changes.rb b/lib/puppet/face/module/changes.rb
index 026661107..602e423dc 100644
--- a/lib/puppet/face/module/changes.rb
+++ b/lib/puppet/face/module/changes.rb
@@ -1,38 +1,38 @@
Puppet::Face.define(:module, '1.0.0') do
action(:changes) do
summary "Show modified files of an installed module."
description <<-EOT
- Show files that have been modified after installation of a given module
- by comparing the on-disk md5 checksum of each file against the module's
- metadata.
+ Shows any files in a module that have been modified since it was
+ installed. This action compares the files on disk to the md5 checksums
+ included in the module's metadata.
EOT
returns "Array of strings representing paths of modified files."
examples <<-EOT
Show modified files of an installed module:
$ puppet module changes /etc/puppet/modules/vcsrepo/
warning: 1 files modified
lib/puppet/provider/vcsrepo.rb
EOT
arguments ""
when_invoked do |path, options|
root_path = Puppet::Module::Tool.find_module_root(path)
Puppet::Module::Tool::Applications::Checksummer.run(root_path, options)
end
when_rendering :console do |return_value|
if return_value.empty?
Puppet.notice "No modified files"
else
Puppet.warning "#{return_value.size} files modified"
end
return_value.map do |changed_file|
"#{changed_file}"
end.join("\n")
end
end
end
diff --git a/lib/puppet/face/module/clean.rb b/lib/puppet/face/module/clean.rb
deleted file mode 100644
index 637263057..000000000
--- a/lib/puppet/face/module/clean.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-Puppet::Face.define(:module, '1.0.0') do
- action(:clean) do
- summary "Clean the module download cache."
- description <<-EOT
- Clean the module download cache.
- EOT
-
- returns <<-EOT
- Return a status Hash:
-
- { :status => "success", :msg => "Cleaned module cache." }
- EOT
-
- examples <<-EOT
- Clean the module download cache:
-
- $ puppet module clean
- Cleaned module cache.
- EOT
-
- when_invoked do |options|
- Puppet::Module::Tool::Applications::Cleaner.run(options)
- end
-
- when_rendering :console do |return_value|
- # Print the status message to the console.
- return_value[:msg]
- end
- end
-end
diff --git a/lib/puppet/face/module/generate.rb b/lib/puppet/face/module/generate.rb
index b9dc354bf..8f1622cd6 100644
--- a/lib/puppet/face/module/generate.rb
+++ b/lib/puppet/face/module/generate.rb
@@ -1,40 +1,42 @@
Puppet::Face.define(:module, '1.0.0') do
action(:generate) do
summary "Generate boilerplate for a new module."
description <<-EOT
- Generate boilerplate for a new module by creating a directory
- pre-populated with a directory structure and files recommended for
- Puppet best practices.
+ Generates boilerplate for a new module by creating the directory
+ structure and files recommended for the Puppet community's best practices.
+
+ A module may need additional directories beyond this boilerplate
+ if it provides plugins, files, or templates.
EOT
returns "Array of Pathname objects representing paths of generated files."
examples <<-EOT
Generate a new module in the current directory:
$ puppet module generate puppetlabs-ssh
notice: Generating module at /Users/kelseyhightower/puppetlabs-ssh
puppetlabs-ssh
puppetlabs-ssh/tests
puppetlabs-ssh/tests/init.pp
puppetlabs-ssh/spec
puppetlabs-ssh/spec/spec_helper.rb
puppetlabs-ssh/spec/spec.opts
puppetlabs-ssh/README
puppetlabs-ssh/Modulefile
puppetlabs-ssh/metadata.json
puppetlabs-ssh/manifests
puppetlabs-ssh/manifests/init.pp
EOT
arguments ""
when_invoked do |name, options|
Puppet::Module::Tool::Applications::Generator.run(name, options)
end
when_rendering :console do |return_value|
return_value.map {|f| f.to_s }.join("\n")
end
end
end
diff --git a/lib/puppet/face/module/install.rb b/lib/puppet/face/module/install.rb
index 8f95ff485..39aab0276 100644
--- a/lib/puppet/face/module/install.rb
+++ b/lib/puppet/face/module/install.rb
@@ -1,83 +1,173 @@
+# encoding: UTF-8
+
Puppet::Face.define(:module, '1.0.0') do
action(:install) do
summary "Install a module from a repository or release archive."
description <<-EOT
- Install a module from a release archive file on-disk or by downloading
- one from a repository. Unpack the archive into the install directory
- specified by the --install-dir option, which defaults to
- #{Puppet.settings[:modulepath].split(File::PATH_SEPARATOR).first}
+ Installs a module from the Puppet Forge, from a release archive file
+ on-disk, or from a private Forge-like repository.
+
+ The specified module will be installed into the directory
+ specified with the `--target-dir` option, which defaults to
+ #{Puppet.settings[:modulepath].split(File::PATH_SEPARATOR).first}.
EOT
returns "Pathname object representing the path to the installed module."
examples <<-EOT
- Install a module from the default repository:
+ Install a module:
+
+ $ puppet module install puppetlabs-vcsrepo
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── puppetlabs-vcsrepo (v0.0.4)
- $ puppet module install puppetlabs/vcsrepo
- notice: Installing puppetlabs-vcsrepo-0.0.4.tar.gz to /etc/puppet/modules/vcsrepo
- /etc/puppet/modules/vcsrepo
+ Install a module to a specific environment:
- Install a specific module version from a repository:
+ $ puppet module install puppetlabs-vcsrepo --environment development
+ Preparing to install into /etc/puppet/environments/development/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/environments/development/modules
+ └── puppetlabs-vcsrepo (v0.0.4)
- $ puppet module install puppetlabs/vcsrepo -v 0.0.4
- notice: Installing puppetlabs-vcsrepo-0.0.4.tar.gz to /etc/puppet/modules/vcsrepo
- /etc/puppet/modules/vcsrepo
+ Install a specific module version:
+
+ $ puppet module install puppetlabs-vcsrepo -v 0.0.4
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── puppetlabs-vcsrepo (v0.0.4)
Install a module into a specific directory:
- $ puppet module install puppetlabs/vcsrepo --install-dir=/usr/share/puppet/modules
- notice: Installing puppetlabs-vcsrepo-0.0.4.tar.gz to /usr/share/puppet/modules/vcsrepo
- /usr/share/puppet/modules/vcsrepo
+ $ puppet module install puppetlabs-vcsrepo --target-dir=/usr/share/puppet/modules
+ Preparing to install into /usr/share/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /usr/share/puppet/modules
+ └── puppetlabs-vcsrepo (v0.0.4)
+
+ Install a module into a specific directory and check for dependencies in other directories:
+
+ $ puppet module install puppetlabs-vcsrepo --target-dir=/usr/share/puppet/modules --modulepath /etc/puppet/modules
+ Preparing to install into /usr/share/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /usr/share/puppet/modules
+ └── puppetlabs-vcsrepo (v0.0.4)
Install a module from a release archive:
$ puppet module install puppetlabs-vcsrepo-0.0.4.tar.gz
- notice: Installing puppetlabs-vcsrepo-0.0.4.tar.gz to /etc/puppet/modules/vcsrepo
- /etc/puppet/modules/vcsrepo
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── puppetlabs-vcsrepo (v0.0.4)
+
+ Install a module from a release archive and ignore dependencies:
+
+ $ puppet module install puppetlabs-vcsrepo-0.0.4.tar.gz --ignore-dependencies
+ Preparing to install into /etc/puppet/modules ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── puppetlabs-vcsrepo (v0.0.4)
+
EOT
arguments ""
option "--force", "-f" do
summary "Force overwrite of existing module, if any."
description <<-EOT
Force overwrite of existing module, if any.
EOT
end
- option "--install-dir=", "-i=" do
- default_to { Puppet.settings[:modulepath].split(File::PATH_SEPARATOR).first }
+ option "--target-dir DIR", "-i DIR" do
summary "The directory into which modules are installed."
description <<-EOT
- The directory into which modules are installed, defaults to the first
+ The directory into which modules are installed; defaults to the first
directory in the modulepath.
+
+ Specifying this option will change the installation directory, and
+ will use the existing modulepath when checking for dependencies. If
+ you wish to check a different set of directories for dependencies, you
+ must also use the `--environment` or `--modulepath` options.
EOT
end
- option "--module-repository=", "-r=" do
- default_to { Puppet.settings[:module_repository] }
- summary "Module repository to use."
+ option "--ignore-dependencies" do
+ summary "Do not attempt to install dependencies"
description <<-EOT
- Module repository to use.
+ Do not attempt to install dependencies.
EOT
end
- option "--version=", "-v=" do
+ option "--modulepath MODULEPATH" do
+ default_to { Puppet.settings[:modulepath] }
+ summary "Which directories to look for modules in"
+ description <<-EOT
+ The list of directories to check for modules. When installing a new
+ module, this setting determines where the module tool will look for
+ its dependencies. If the `--target dir` option is not specified, the
+ first directory in the modulepath will also be used as the install
+ directory.
+
+ When installing a module into an environment whose modulepath is
+ specified in puppet.conf, you can use the `--environment` option
+ instead, and its modulepath will be used automatically.
+
+ This setting should be a list of directories separated by the path
+ separator character. (The path separator is `:` on Unix-like platforms
+ and `;` on Windows.)
+ EOT
+ end
+
+ option "--version VER", "-v VER" do
summary "Module version to install."
description <<-EOT
- Module version to install, can be a requirement string, eg '>= 1.0.3',
- defaults to latest version.
+ Module version to install; can be an exact version or a requirement string,
+ eg '>= 1.0.3'. Defaults to latest version.
+ EOT
+ end
+
+ option "--environment NAME" do
+ default_to { "production" }
+ summary "The target environment to install modules into."
+ description <<-EOT
+ The target environment to install modules into. Only applicable if
+ multiple environments (with different modulepaths) have been
+ configured in puppet.conf.
EOT
end
when_invoked do |name, options|
+ sep = File::PATH_SEPARATOR
+ if options[:target_dir]
+ options[:modulepath] = "#{options[:target_dir]}#{sep}#{options[:modulepath]}"
+ end
+
+ Puppet.settings[:modulepath] = options[:modulepath]
+ options[:target_dir] = Puppet.settings[:modulepath].split(sep).first
+
+ Puppet.notice "Preparing to install into #{options[:target_dir]} ..."
Puppet::Module::Tool::Applications::Installer.run(name, options)
end
- when_rendering :console do |return_value|
- # Get the string representation of the Pathname object and print it to
- # the console.
- return_value.to_s
+ when_rendering :console do |return_value, name, options|
+ if return_value[:result] == :failure
+ Puppet.err(return_value[:error][:multiline])
+ exit 1
+ else
+ tree = Puppet::Module::Tool.format_tree(return_value[:installed_modules], return_value[:install_dir])
+ return_value[:install_dir] + "\n" +
+ Puppet::Module::Tool.build_tree(tree)
+ end
end
end
end
diff --git a/lib/puppet/face/module/list.rb b/lib/puppet/face/module/list.rb
index 7162dfe46..2126474ed 100644
--- a/lib/puppet/face/module/list.rb
+++ b/lib/puppet/face/module/list.rb
@@ -1,84 +1,285 @@
+# encoding: UTF-8
+
Puppet::Face.define(:module, '1.0.0') do
action(:list) do
summary "List installed modules"
description <<-HEREDOC
- List puppet modules from a specific environment, specified modulepath or
- default to listing modules in the default modulepath. The output will
- include information about unmet module dependencies based on information
- from module metadata.
- #{Puppet.settings[:modulepath]}
+ Lists the installed puppet modules. By default, this action scans the
+ modulepath from puppet.conf's `[main]` block; use the --modulepath
+ option to change which directories are scanned.
+
+ The output of this action includes information from the module's
+ metadata, including version numbers and unmet module dependencies.
HEREDOC
returns "hash of paths to module objects"
- option "--env ENVIRONMENT" do
+ option "--environment NAME" do
+ default_to { "production" }
summary "Which environments' modules to list"
+ description <<-EOT
+ Which environments' modules to list.
+ EOT
end
option "--modulepath MODULEPATH" do
summary "Which directories to look for modules in"
+ description <<-EOT
+ Which directories to look for modules in; use the system path separator
+ character (`:` on Unix-like systems and `;` on Windows) to specify
+ multiple directories.
+ EOT
+ end
+
+ option "--tree" do
+ summary "Whether to show dependencies as a tree view"
end
examples <<-EOT
List installed modules:
$ puppet module list
/etc/puppet/modules
- bacula (0.0.2)
- /usr/share/puppet/modules
- apache (0.0.3)
- bacula (0.0.1)
+ ├── bodepd-create_resources (v0.0.1)
+ ├── puppetlabs-bacula (v0.0.2)
+ ├── puppetlabs-mysql (v0.0.1)
+ ├── puppetlabs-sqlite (v0.0.1)
+ └── puppetlabs-stdlib (v2.2.1)
+ /usr/share/puppet/modules (no modules installed)
- List installed modules from a specified environment:
+ List installed modules in a tree view:
- $ puppet module list --env 'test'
- Missing dependency `stdlib`:
- `rrd` (0.0.2) requires `puppetlabs/stdlib` (>= 2.2.0)
+ $ puppet module list --tree
+ /etc/puppet/modules
+ └─┬ puppetlabs-bacula (v0.0.2)
+ ├── puppetlabs-stdlib (v2.2.1)
+ ├─┬ puppetlabs-mysql (v0.0.1)
+ │ └── bodepd-create_resources (v0.0.1)
+ └── puppetlabs-sqlite (v0.0.1)
+ /usr/share/puppet/modules (no modules installed)
+
+ List installed modules from a specified environment:
- /tmp/puppet/modules
- rrd (0.0.2)
+ $ puppet module list --environment production
+ /etc/puppet/modules
+ ├── bodepd-create_resources (v0.0.1)
+ ├── puppetlabs-bacula (v0.0.2)
+ ├── puppetlabs-mysql (v0.0.1)
+ ├── puppetlabs-sqlite (v0.0.1)
+ └── puppetlabs-stdlib (v2.2.1)
+ /usr/share/puppet/modules (no modules installed)
List installed modules from a specified modulepath:
- $ puppet module list --modulepath /tmp/facts1:/tmp/facts2
- /tmp/facts1
- stdlib
- /tmp/facts2
- nginx (1.0.0)
+ $ puppet module list --modulepath /usr/share/puppet/modules
+ /usr/share/puppet/modules (no modules installed)
EOT
when_invoked do |options|
Puppet[:modulepath] = options[:modulepath] if options[:modulepath]
- environment = Puppet::Node::Environment.new(options[:env])
+ environment = Puppet::Node::Environment.new(options[:environment])
environment.modules_by_path
end
when_rendering :console do |modules_by_path, options|
output = ''
Puppet[:modulepath] = options[:modulepath] if options[:modulepath]
- environment = Puppet::Node::Environment.new(options[:env])
+ environment = Puppet::Node::Environment.new(options[:production])
- dependency_errors = false
+ error_types = {
+ :non_semantic_version => {
+ :title => "Non semantic version dependency"
+ },
+ :missing => {
+ :title => "Missing dependency"
+ },
+ :version_mismatch => {
+ :title => "Module '%s' (v%s) fails to meet some dependencies:"
+ }
+ }
+ @unmet_deps = {}
+ error_types.each_key do |type|
+ @unmet_deps[type] = Hash.new do |hash, key|
+ hash[key] = { :errors => [], :parent => nil }
+ end
+ end
+
+ # Prepare the unmet dependencies for display on the console.
environment.modules.sort_by {|mod| mod.name}.each do |mod|
- mod.unmet_dependencies.sort_by {|dep| dep[:name]}.each do |dep|
- dependency_errors = true
- $stderr.puts dep[:error]
+ unmet_grouped = Hash.new { |h,k| h[k] = [] }
+ unmet_grouped = mod.unmet_dependencies.inject(unmet_grouped) do |acc, dep|
+ acc[dep[:reason]] << dep
+ acc
+ end
+ unmet_grouped.each do |type, deps|
+ unless deps.empty?
+ unmet_grouped[type].sort_by { |dep| dep[:name] }.each do |dep|
+ dep_name = dep[:name].gsub('/', '-')
+ installed_version = dep[:mod_details][:installed_version]
+ version_constraint = dep[:version_constraint]
+ parent_name = dep[:parent][:name].gsub('/', '-')
+ parent_version = dep[:parent][:version]
+
+ msg = "'#{parent_name}' (#{parent_version})"
+ msg << " requires '#{dep_name}' (#{version_constraint})"
+ @unmet_deps[type][dep[:name]][:errors] << msg
+ @unmet_deps[type][dep[:name]][:parent] = {
+ :name => dep[:parent][:name],
+ :version => parent_version
+ }
+ @unmet_deps[type][dep[:name]][:version] = installed_version
+ end
+ end
+ end
+ end
+
+ # Display unmet dependencies by category.
+ error_display_order = [:non_semantic_version, :version_mismatch, :missing]
+ error_display_order.each do |type|
+ unless @unmet_deps[type].empty?
+ @unmet_deps[type].keys.sort_by {|dep| dep }.each do |dep|
+ name = dep.gsub('/', '-')
+ title = error_types[type][:title]
+ errors = @unmet_deps[type][dep][:errors]
+ version = @unmet_deps[type][dep][:version]
+
+ msg = case type
+ when :version_mismatch
+ title % [name, version] + "\n"
+ when :non_semantic_version
+ title + " '#{name}' (v#{version}):\n"
+ else
+ title + " '#{name}':\n"
+ end
+
+ errors.each { |error_string| msg << " #{error_string}\n" }
+ Puppet.warning msg.chomp
+ end
end
end
- output << "\n" if dependency_errors
+ environment.modulepath.each do |path|
+ modules = modules_by_path[path]
+ no_mods = modules.empty? ? ' (no modules installed)' : ''
+ output << "#{path}#{no_mods}\n"
- modules_by_path.each do |path, modules|
- output << "#{path}\n"
- modules.sort_by {|mod| mod.name }.each do |mod|
- version_string = mod.version ? "(#{mod.version})" : ''
- output << " #{mod.name} #{version_string}\n"
+ if options[:tree]
+ # The modules with fewest things depending on them will be the
+ # parent of the tree. Can't assume to start with 0 dependencies
+ # since dependencies may be cyclical.
+ modules_by_num_requires = modules.sort_by {|m| m.required_by.size}
+ @seen = {}
+ tree = list_format_tree(modules_by_num_requires, [], nil,
+ :label_unmet => true, :path => path, :label_invalid => false)
+ else
+ tree = []
+ modules.sort_by { |mod| mod.forge_name or mod.name }.each do |mod|
+ tree << list_format_node(mod, path, :label_unmet => false,
+ :path => path, :label_invalid => true)
+ end
end
+
+ output << Puppet::Module::Tool.build_tree(tree)
end
+
output
end
+ end
+
+ # Prepare a list of module objects and their dependencies for print in a
+ # tree view.
+ #
+ # Returns an Array of Hashes
+ #
+ # Example:
+ #
+ # [
+ # {
+ # :text => "puppetlabs-bacula (v0.0.2)",
+ # :dependencies=> [
+ # { :text => "puppetlabs-stdlib (v2.2.1)", :dependencies => [] },
+ # {
+ # :text => "puppetlabs-mysql (v1.0.0)"
+ # :dependencies => [
+ # {
+ # :text => "bodepd-create_resources (v0.0.1)",
+ # :dependencies => []
+ # }
+ # ]
+ # },
+ # { :text => "puppetlabs-sqlite (v0.0.1)", :dependencies => [] },
+ # ]
+ # }
+ # ]
+ #
+ # When the above data structure is passed to Puppet::Module::Tool.build_tree
+ # you end up with something like this:
+ #
+ # /etc/puppet/modules
+ # └─┬ puppetlabs-bacula (v0.0.2)
+ # ├── puppetlabs-stdlib (v2.2.1)
+ # ├─┬ puppetlabs-mysql (v1.0.0)
+ # │ └── bodepd-create_resources (v0.0.1)
+ # └── puppetlabs-sqlite (v0.0.1)
+ #
+ def list_format_tree(list, ancestors=[], parent=nil, params={})
+ list.map do |mod|
+ next if @seen[(mod.forge_name or mod.name)]
+ node = list_format_node(mod, parent, params)
+ @seen[(mod.forge_name or mod.name)] = true
+
+ unless ancestors.include?(mod)
+ node[:dependencies] ||= []
+ missing_deps = mod.unmet_dependencies.select do |dep|
+ dep[:reason] == :missing
+ end
+ missing_deps.map do |mis_mod|
+ str = "#{colorize(:bg_red, 'UNMET DEPENDENCY')} #{mis_mod[:name].gsub('/', '-')} "
+ str << "(#{colorize(:cyan, mis_mod[:version_constraint])})"
+ node[:dependencies] << { :text => str }
+ end
+ node[:dependencies] += list_format_tree(mod.dependencies_as_modules,
+ ancestors + [mod], mod, params)
+ end
+
+ node
+ end.compact
+ end
+
+ # Prepare a module object for print in a tree view. Each node in the tree
+ # must be a Hash in the following format:
+ #
+ # { :text => "puppetlabs-mysql (v1.0.0)" }
+ #
+ # The value of a module's :text is affected by three (3) factors: the format
+ # of the tree, it's dependency status, and the location in the modulepath
+ # relative to it's parent.
+ #
+ # Returns a Hash
+ #
+ def list_format_node(mod, parent, params)
+ str = ''
+ str << (mod.forge_name ? mod.forge_name.gsub('/', '-') : mod.name)
+ str << ' (' + colorize(:cyan, mod.version ? "v#{mod.version}" : '???') + ')'
+
+ unless File.dirname(mod.path) == params[:path]
+ str << " [#{File.dirname(mod.path)}]"
+ end
+
+ if @unmet_deps[:version_mismatch].include?(mod.forge_name)
+ if params[:label_invalid]
+ str << ' ' + colorize(:red, 'invalid')
+ elsif parent.respond_to?(:forge_name)
+ unmet_parent = @unmet_deps[:version_mismatch][mod.forge_name][:parent]
+ if (unmet_parent[:name] == parent.forge_name &&
+ unmet_parent[:version] == "v#{parent.version}")
+ str << ' ' + colorize(:red, 'invalid')
+ end
+ end
+ end
+ { :text => str }
end
end
diff --git a/lib/puppet/face/module/search.rb b/lib/puppet/face/module/search.rb
index cec8d9089..0c488082f 100644
--- a/lib/puppet/face/module/search.rb
+++ b/lib/puppet/face/module/search.rb
@@ -1,55 +1,90 @@
+require 'puppet/util/terminal'
+
Puppet::Face.define(:module, '1.0.0') do
action(:search) do
summary "Search a repository for a module."
description <<-EOT
- Search a repository for modules whose names match a specific substring.
+ Searches a repository for modules whose names, descriptions, or keywords
+ match the provided search term.
EOT
returns "Array of module metadata hashes"
examples <<-EOT
Search the default repository for a module:
$ puppet module search puppetlabs
NAME DESCRIPTION AUTHOR KEYWORDS
bacula This is a generic Apache module @puppetlabs backups
EOT
- arguments ""
-
- option "--module-repository=", "-r=" do
- default_to { Puppet.settings[:module_repository] }
- summary "Module repository to use."
- description <<-EOT
- Module repository to use.
- EOT
- end
+ arguments ""
when_invoked do |term, options|
+ server = Puppet.settings[:module_repository].sub(/^(?!https?:\/\/)/, 'http://')
+ Puppet.notice "Searching #{server} ..."
Puppet::Module::Tool::Applications::Searcher.run(term, options)
end
- when_rendering :console do |return_value|
+ when_rendering :console do |results, term, options|
+ return "No results found for '#{term}'." if results.empty?
+
+ padding = ' '
+ headers = {
+ 'full_name' => 'NAME',
+ 'desc' => 'DESCRIPTION',
+ 'author' => 'AUTHOR',
+ 'tag_list' => 'KEYWORDS',
+ }
- FORMAT = "%-10s %-32s %-14s %s\n"
+ min_widths = Hash[ *headers.map { |k,v| [k, v.length] }.flatten ]
+ min_widths['full_name'] = min_widths['author'] = 12
- def header
- FORMAT % ['NAME', 'DESCRIPTION', 'AUTHOR', 'KEYWORDS']
+ min_width = min_widths.inject(0) { |sum,pair| sum += pair.last } + (padding.length * (headers.length - 1))
+
+ terminal_width = [Puppet::Util::Terminal.width, min_width].max
+
+ columns = results.inject(min_widths) do |hash, result|
+ {
+ 'full_name' => [ hash['full_name'], result['full_name'].length ].max,
+ 'desc' => [ hash['desc'], result['desc'].length ].max,
+ 'author' => [ hash['author'], "@#{result['author']}".length ].max,
+ 'tag_list' => [ hash['tag_list'], result['tag_list'].join(' ').length ].max,
+ }
end
- def format_row(name, description, author, tag_list)
- keywords = tag_list.join(' ')
- FORMAT % [name[0..10], description[0..32], "@#{author[0..14]}", keywords]
+ flex_width = terminal_width - columns['full_name'] - columns['author'] - (padding.length * (headers.length - 1))
+ tag_lists = results.map { |r| r['tag_list'] }
+
+ while (columns['tag_list'] > flex_width / 3)
+ longest_tag_list = tag_lists.sort_by { |tl| tl.join(' ').length }.last
+ break if [ [], [term] ].include? longest_tag_list
+ longest_tag_list.delete(longest_tag_list.sort_by { |t| t == term ? -1 : t.length }.last)
+ columns['tag_list'] = tag_lists.map { |tl| tl.join(' ').length }.max
end
- output = ''
- output << header unless return_value.empty?
+ columns['tag_list'] = [
+ flex_width / 3,
+ tag_lists.map { |tl| tl.join(' ').length }.max,
+ ].max
+ columns['desc'] = flex_width - columns['tag_list']
+
+ format = %w{full_name desc author tag_list}.map do |k|
+ "%-#{ [ columns[k], min_widths[k] ].max }s"
+ end.join(padding) + "\n"
- return_value.map do |match|
- output << format_row(match['name'], match['desc'], match['author'], match['tag_list'])
+ highlight = proc do |s|
+ s = s.gsub(term, colorize(:green, term))
+ s = s.gsub(term.gsub('/', '-'), colorize(:green, term.gsub('/', '-'))) if term =~ /\//
+ s
end
- output
+ format % [ headers['full_name'], headers['desc'], headers['author'], headers['tag_list'] ] +
+ results.map do |match|
+ name, desc, author, keywords = %w{full_name desc author tag_list}.map { |k| match[k] }
+ desc = desc[0...(columns['desc'] - 3)] + '...' if desc.length > columns['desc']
+ highlight[format % [ name.sub('/', '-'), desc, "@#{author}", [keywords].flatten.join(' ') ]]
+ end.join
end
end
end
diff --git a/lib/puppet/face/module/uninstall.rb b/lib/puppet/face/module/uninstall.rb
index 802507c58..1effa6c38 100644
--- a/lib/puppet/face/module/uninstall.rb
+++ b/lib/puppet/face/module/uninstall.rb
@@ -1,91 +1,86 @@
Puppet::Face.define(:module, '1.0.0') do
action(:uninstall) do
summary "Uninstall a puppet module."
description <<-EOT
- Uninstall a puppet module from the modulepath or a specific
- target directory which defaults to
- #{Puppet.settings[:modulepath].split(File::PATH_SEPARATOR).join(', ')}.
+ 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 from all directories in the modulepath:
+ Uninstall a module:
- $ puppet module uninstall ssh
+ $ puppet module uninstall puppetlabs-ssh
Removed /etc/puppet/modules/ssh (v1.0.0)
Uninstall a module from a specific directory:
- $ puppet module uninstall --modulepath /usr/share/puppet/modules ssh
+ $ 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 --environment development
+ $ 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 --version 2.0.0 ssh
+ $ puppet module uninstall puppetlabs-ssh --version 2.0.0
Removed /etc/puppet/modules/ssh (v2.0.0)
EOT
arguments ""
- option "--environment=NAME", "--env=NAME" do
+ 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 "--environment NAME" do
default_to { "production" }
- summary "The target environment to search for modules."
+ summary "The target environment to uninstall modules from."
description <<-EOT
- The target environment to search for modules.
+ The target environment to uninstall modules from.
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
- that matches the specified version must be installed or an error is raised.
+ 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
option "--modulepath=" do
summary "The target directory to search for modules."
description <<-EOT
The target directory to search for modules.
EOT
end
when_invoked do |name, options|
- if options[:modulepath]
- unless File.directory?(options[:modulepath])
- raise ArgumentError, "Directory #{options[:modulepath]} does not exist"
- end
- end
-
Puppet[:modulepath] = options[:modulepath] if options[:modulepath]
- options[:name] = name
+ name = name.gsub('/', '-')
+ Puppet.notice "Preparing to uninstall '#{name}'" << (options[:version] ? " (#{colorize(:cyan, options[:version].sub(/^(?=\d)/, 'v'))})" : '') << " ..."
Puppet::Module::Tool::Applications::Uninstaller.run(name, options)
end
when_rendering :console do |return_value|
- output = ''
-
- return_value[:removed_mods].each do |mod|
- output << "Removed #{mod.path} (v#{mod.version})\n"
+ 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
-
- return_value[:errors].map do |mod_name, errors|
- if ! errors.empty?
- header = "Could not uninstall module #{return_value[:options][:name]}"
- header << " (v#{return_value[:options][:version]})" if return_value[:options][:version]
- output << "#{header}:\n"
- errors.map { |error| output << " #{error}\n" }
- end
- end
-
- output
end
end
end
diff --git a/lib/puppet/face/module/upgrade.rb b/lib/puppet/face/module/upgrade.rb
new file mode 100644
index 000000000..eac79c185
--- /dev/null
+++ b/lib/puppet/face/module/upgrade.rb
@@ -0,0 +1,84 @@
+# 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."
+ description <<-EOT
+ Force the upgrade of an installed module even if there are local
+ changes or the possibility of causing broken dependencies.
+ EOT
+ end
+
+ option "--ignore-dependencies" do
+ summary "Do not attempt to install dependencies."
+ description <<-EOT
+ Do not attempt to install dependencies
+ EOT
+ end
+
+ option "--environment NAME" do
+ default_to { "production" }
+ summary "The target environment to search for modules."
+ description <<-EOT
+ The target environment to search for modules.
+ 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::Module::Tool::Applications::Upgrader.new(name, options).run
+ end
+
+ when_rendering :console do |return_value|
+ if return_value[:result] == :failure
+ Puppet.err(return_value[:error][:multiline])
+ exit 1
+ elsif return_value[:result] == :noop
+ Puppet.err(return_value[:error][:multiline])
+ exit 0
+ else
+ tree = Puppet::Module::Tool.format_tree(return_value[:affected_modules], return_value[:base_dir])
+ return_value[:base_dir] + "\n" +
+ Puppet::Module::Tool.build_tree(tree)
+ end
+ end
+ end
+end
diff --git a/lib/puppet/forge.rb b/lib/puppet/forge.rb
index 6b3c5742f..8eeb991ae 100644
--- a/lib/puppet/forge.rb
+++ b/lib/puppet/forge.rb
@@ -1,153 +1,96 @@
require 'net/http'
require 'open-uri'
require 'pathname'
require 'uri'
require 'puppet/forge/cache'
require 'puppet/forge/repository'
module Puppet::Forge
- class Forge
- def initialize(url=Puppet.settings[:module_repository])
- @uri = URI.parse(url)
+ # Return a list of module metadata hashes that match the search query.
+ # This return value is used by the module_tool face install search,
+ # and displayed to on the console.
+ #
+ # Example return value:
+ #
+ # [
+ # {
+ # "author" => "puppetlabs",
+ # "name" => "bacula",
+ # "tag_list" => ["backup", "bacula"],
+ # "releases" => [{"version"=>"0.0.1"}, {"version"=>"0.0.2"}],
+ # "full_name" => "puppetlabs/bacula",
+ # "version" => "0.0.2",
+ # "project_url" => "http://github.com/puppetlabs/puppetlabs-bacula",
+ # "desc" => "bacula"
+ # }
+ # ]
+ #
+ def self.search(term)
+ request = Net::HTTP::Get.new("/modules.json?q=#{URI.escape(term)}")
+ response = repository.make_http_request(request)
+
+ case response.code
+ when "200"
+ matches = PSON.parse(response.body)
+ else
+ raise RuntimeError, "Could not execute search (HTTP #{response.code})"
+ matches = []
end
- # Return a list of module metadata hashes that match the search query.
- # This return value is used by the module_tool face install search,
- # and displayed to on the console.
- #
- # Example return value:
- #
- # [
- # {
- # "author" => "puppetlabs",
- # "name" => "bacula",
- # "tag_list" => ["backup", "bacula"],
- # "releases" => [{"version"=>"0.0.1"}, {"version"=>"0.0.2"}],
- # "full_name" => "puppetlabs/bacula",
- # "version" => "0.0.2",
- # "project_url" => "http://github.com/puppetlabs/puppetlabs-bacula",
- # "desc" => "bacula"
- # }
- # ]
- #
- def search(term)
- request = Net::HTTP::Get.new("/modules.json?q=#{URI.escape(term)}")
- response = repository.make_http_request(request)
+ matches
+ end
- case response.code
- when "200"
- matches = PSON.parse(response.body)
+ def self.remote_dependency_info(author, mod_name, version)
+ version_string = version ? "&version=#{version}" : ''
+ request = Net::HTTP::Get.new("/api/v1/releases.json?module=#{author}/#{mod_name}" + version_string)
+ response = repository.make_http_request(request)
+ json = PSON.parse(response.body) rescue {}
+ case response.code
+ when "200"
+ return json
+ else
+ error = json['error'] || ''
+ if error =~ /^Module #{author}\/#{mod_name} has no release/
+ return []
else
- raise RuntimeError, "Could not execute search (HTTP #{response.code})"
- matches = []
+ raise RuntimeError, "Could not find release information for this module (#{author}/#{mod_name}) (HTTP #{response.code})"
end
-
- matches
end
+ end
- # Return a Pathname object representing the path to the module
- # release package in the `Puppet.settings[:module_working_dir]`.
- def get_release_package(params)
+ def self.get_release_packages_from_repository(install_list)
+ install_list.map do |release|
+ modname, version, file = release
cache_path = nil
- case params[:source]
- when :repository
- if not (params[:author] && params[:modname])
- raise ArgumentError, ":author and :modename required"
- end
- cache_path = get_release_package_from_repository(params[:author], params[:modname], params[:version])
- when :filesystem
- if not params[:filename]
- raise ArgumentError, ":filename required"
- end
- cache_path = get_release_package_from_filesystem(params[:filename])
- else
- raise ArgumentError, "Could not determine installation source"
- end
-
- cache_path
- end
-
- def get_releases(author, modname)
- request_string = "/#{author}/#{modname}"
-
- begin
- response = repository.make_http_request(request_string)
- rescue => e
- raise ArgumentError, "Could not find a release for this module (#{e.message})"
- end
-
- results = PSON.parse(response.body)
- # At this point releases look like this:
- # [{"version" => "0.0.1"}, {"version" => "0.0.2"},{"version" => "0.0.3"}]
- #
- # Lets fix this up a bit and return something like this to the caller
- # ["0.0.1", "0.0.2", "0.0.3"]
- results["releases"].collect {|release| release["version"]}
- end
-
- private
-
- # Locate and download a module release package from the remote forge
- # repository into the `Puppet.settings[:module_working_dir]`. Do not
- # unpack it, just return the location of the package on disk.
- def get_release_package_from_repository(author, modname, version=nil)
- release = get_release(author, modname, version)
- if release['file']
+ if file
begin
- cache_path = repository.retrieve(release['file'])
+ cache_path = repository.retrieve(file)
rescue OpenURI::HTTPError => e
raise RuntimeError, "Could not download module: #{e.message}"
end
else
raise RuntimeError, "Malformed response from module repository."
end
-
- cache_path
- end
-
- # Locate a module release package on the local filesystem and move it
- # into the `Puppet.settings[:module_working_dir]`. Do not unpack it, just
- # return the location of the package on disk.
- def get_release_package_from_filesystem(filename)
- if File.exist?(File.expand_path(filename))
- repository = Repository.new('file:///')
- uri = URI.parse("file://#{URI.escape(File.expand_path(filename))}")
- cache_path = repository.retrieve(uri)
- else
- raise ArgumentError, "File does not exists: #{filename}"
- end
-
cache_path
end
+ end
- def repository
- @repository ||= Puppet::Forge::Repository.new(@uri)
+ # Locate a module release package on the local filesystem and move it
+ # into the `Puppet.settings[:module_working_dir]`. Do not unpack it, just
+ # return the location of the package on disk.
+ def self.get_release_package_from_filesystem(filename)
+ if File.exist?(File.expand_path(filename))
+ repository = Repository.new('file:///')
+ uri = URI.parse("file://#{URI.escape(File.expand_path(filename))}")
+ cache_path = repository.retrieve(uri)
+ else
+ raise ArgumentError, "File does not exists: #{filename}"
end
- # Connect to the remote repository and locate a specific module release
- # by author/name combination. If a version requirement is specified, search
- # for that exact version, or grab the latest release available.
- #
- # Return the following response to the caller:
- #
- # {"file"=>"/system/releases/p/puppetlabs/puppetlabs-apache-0.0.3.tar.gz", "version"=>"0.0.3"}
- #
- #
- def get_release(author, modname, version_requirement=nil)
- request_string = "/users/#{author}/modules/#{modname}/releases/find.json"
- if version_requirement
- request_string + "?version=#{URI.escape(version_requirement)}"
- end
- request = Net::HTTP::Get.new(request_string)
-
- begin
- response = repository.make_http_request(request)
- rescue => e
- raise ArgumentError, "Could not find a release for this module (#{e.message})"
- end
+ cache_path
+ end
- PSON.parse(response.body)
- end
+ def self.repository
+ @repository ||= Puppet::Forge::Repository.new
end
end
-
diff --git a/lib/puppet/forge/cache.rb b/lib/puppet/forge/cache.rb
index 253f24761..592bf1ace 100644
--- a/lib/puppet/forge/cache.rb
+++ b/lib/puppet/forge/cache.rb
@@ -1,55 +1,55 @@
require 'uri'
module Puppet::Forge
# = Cache
#
# Provides methods for reading files from local cache, filesystem or network.
class Cache
# Instantiate new cahe for the +repositry+ instance.
def initialize(repository, options = {})
@repository = repository
@options = options
end
# Return filename retrieved from +uri+ instance. Will download this file and
# cache it if needed.
#
# TODO: Add checksum support.
# TODO: Add error checking.
def retrieve(url)
(path + File.basename(url.to_s)).tap do |cached_file|
uri = url.is_a?(::URI) ? url : ::URI.parse(url)
unless cached_file.file?
if uri.scheme == 'file'
FileUtils.cp(URI.unescape(uri.path), cached_file)
else
# TODO: Handle HTTPS; probably should use repository.contact
data = read_retrieve(uri)
cached_file.open('wb') { |f| f.write data }
end
end
end
end
# Return contents of file at the given URI's +uri+.
def read_retrieve(uri)
return uri.read
end
# Return Pathname for repository's cache directory, create it if needed.
def path
- return @path ||= (self.class.base_path + @repository.cache_key).tap{ |o| o.mkpath }
+ (self.class.base_path + @repository.cache_key).tap{ |o| o.mkpath }
end
# Return the base Pathname for all the caches.
def self.base_path
Pathname(Puppet.settings[:module_working_dir]) + 'cache'
end
# Clean out all the caches.
def self.clean
base_path.rmtree if base_path.exist?
end
end
end
diff --git a/lib/puppet/forge/repository.rb b/lib/puppet/forge/repository.rb
index 2e101496b..fd92b7d44 100644
--- a/lib/puppet/forge/repository.rb
+++ b/lib/puppet/forge/repository.rb
@@ -1,121 +1,102 @@
require 'net/http'
require 'digest/sha1'
require 'uri'
-require 'puppet/module_tool/utils'
-
module Puppet::Forge
- # Directory names that should not be checksummed.
- ARTIFACTS = ['pkg', /^\./, /^~/, /^#/, 'coverage']
- FULL_MODULE_NAME_PATTERN = /\A([^-\/|.]+)[-|\/](.+)\z/
- REPOSITORY_URL = Puppet.settings[:module_repository]
-
# = Repository
#
# This class is a file for accessing remote repositories with modules.
class Repository
- include Puppet::Module::Tool::Utils::Interrogation
attr_reader :uri, :cache
# Instantiate a new repository instance rooted at the optional string
# +url+, else an instance of the default Puppet modules repository.
def initialize(url=Puppet[:module_repository])
- @uri = url.is_a?(::URI) ? url : ::URI.parse(url)
+ @uri = url.is_a?(::URI) ? url : ::URI.parse(url.sub(/^(?!https?:\/\/)/, 'http://'))
@cache = Cache.new(self)
end
# Read HTTP proxy configurationm from Puppet's config file, or the
# http_proxy environment variable.
def http_proxy_env
proxy_env = ENV["http_proxy"] || ENV["HTTP_PROXY"] || nil
begin
return URI.parse(proxy_env) if proxy_env
rescue URI::InvalidURIError
return nil
end
return nil
end
def http_proxy_host
env = http_proxy_env
if env and env.host then
return env.host
end
if Puppet.settings[:http_proxy_host] == 'none'
return nil
end
return Puppet.settings[:http_proxy_host]
end
def http_proxy_port
env = http_proxy_env
if env and env.port then
return env.port
end
return Puppet.settings[:http_proxy_port]
end
# Return a Net::HTTPResponse read for this +request+.
- #
- # Options:
- # * :authenticate => Request authentication on the terminal. Defaults to false.
def make_http_request(request, options = {})
- if options[:authenticate]
- authenticate(request)
- end
if ! @uri.user.nil? && ! @uri.password.nil?
request.basic_auth(@uri.user, @uri.password)
end
return read_response(request)
end
# Return a Net::HTTPResponse read from this HTTPRequest +request+.
def read_response(request)
begin
Net::HTTP::Proxy(
http_proxy_host,
http_proxy_port
).start(@uri.host, @uri.port) do |http|
http.request(request)
end
rescue Errno::ECONNREFUSED, SocketError
- raise RuntimeError, "Could not reach remote repository"
+ msg = "Error: Could not connect to #{@uri}\n"
+ msg << " There was a network communications problem\n"
+ msg << " Check your network connection and try again\n"
+ $stderr << msg
+ exit(1)
end
end
- # Set the HTTP Basic Authentication parameters for the Net::HTTPRequest
- # +request+ by asking the user for input on the console.
- def authenticate(request)
- Puppet.notice "Authenticating for #{@uri}"
- email = prompt('Email Address')
- password = prompt('Password', true)
- request.basic_auth(email, password)
- end
-
# Return the local file name containing the data downloaded from the
# repository at +release+ (e.g. "myuser-mymodule").
def retrieve(release)
return cache.retrieve(@uri + release)
end
# Return the URI string for this repository.
def to_s
return @uri.to_s
end
# Return the cache key for this repository, this a hashed string based on
# the URI.
def cache_key
return @cache_key ||= [
@uri.to_s.gsub(/[^[:alnum:]]+/, '_').sub(/_$/, ''),
Digest::SHA1.hexdigest(@uri.to_s)
].join('-')
end
end
end
diff --git a/lib/puppet/module_tool.rb b/lib/puppet/module_tool.rb
index 6d8d4dbd3..6ac1eb9e5 100644
--- a/lib/puppet/module_tool.rb
+++ b/lib/puppet/module_tool.rb
@@ -1,61 +1,101 @@
+# encoding: UTF-8
# Load standard libraries
require 'pathname'
require 'fileutils'
-require 'puppet/module_tool/utils'
+require 'puppet/util/colors'
# Define tool
module Puppet
class Module
module Tool
+ extend Puppet::Util::Colors
- # Directory names that should not be checksummed.
- ARTIFACTS = ['pkg', /^\./, /^~/, /^#/, 'coverage']
+ # 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
def self.find_module_root(path)
for dir in [path, Dir.pwd].compact
if File.exist?(File.join(dir, 'Modulefile'))
return dir
end
end
raise ArgumentError, "Could not find a valid module at #{path ? path.inspect : 'current directory'}"
end
+
+ # Builds a formatted tree from a list of node hashes containing +:text+
+ # and +:dependencies+ keys.
+ def self.build_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 = build_tree(deps, level + 1)
+ branch.gsub!(/^#{indent} /, indent + '│') unless last_node
+ str << branch
+ end
+
+ return str
+ end
+
+ def self.format_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
+ format_tree(mod[:dependencies], dir)
+ end
+ 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/lib/puppet/module_tool/applications.rb b/lib/puppet/module_tool/applications.rb
index d5eb7f581..c3fb85731 100644
--- a/lib/puppet/module_tool/applications.rb
+++ b/lib/puppet/module_tool/applications.rb
@@ -1,17 +1,17 @@
require 'puppet/module'
class Puppet::Module
module Tool
module Applications
require 'puppet/module_tool/applications/application'
require 'puppet/module_tool/applications/builder'
require 'puppet/module_tool/applications/checksummer'
- require 'puppet/module_tool/applications/cleaner'
require 'puppet/module_tool/applications/generator'
require 'puppet/module_tool/applications/installer'
require 'puppet/module_tool/applications/searcher'
require 'puppet/module_tool/applications/unpacker'
require 'puppet/module_tool/applications/uninstaller'
+ require 'puppet/module_tool/applications/upgrader'
end
end
end
diff --git a/lib/puppet/module_tool/applications/application.rb b/lib/puppet/module_tool/applications/application.rb
index fd398da81..bce0e84b1 100644
--- a/lib/puppet/module_tool/applications/application.rb
+++ b/lib/puppet/module_tool/applications/application.rb
@@ -1,80 +1,82 @@
require 'net/http'
require 'semver'
-require 'puppet/module_tool/utils/interrogation'
+require 'puppet/util/colors'
module Puppet::Module::Tool
module Applications
class Application
- include Puppet::Module::Tool::Utils::Interrogation
+ include Puppet::Util::Colors
def self.run(*args)
new(*args).run
end
attr_accessor :options
def initialize(options = {})
+ if Puppet.features.microsoft_windows?
+ raise Puppet::Error, "`puppet module` actions are currently not supported on Microsoft Windows"
+ end
@options = options
end
def run
raise NotImplementedError, "Should be implemented in child classes."
end
def discuss(response, success, failure)
case response
when Net::HTTPOK, Net::HTTPCreated
Puppet.notice success
else
errors = PSON.parse(response.body)['error'] rescue "HTTP #{response.code}, #{response.body}"
Puppet.warning "#{failure} (#{errors})"
end
end
def metadata(require_modulefile = false)
unless @metadata
unless @path
raise ArgumentError, "Could not determine module path"
end
@metadata = Puppet::Module::Tool::Metadata.new
contents = ContentsDescription.new(@path)
contents.annotate(@metadata)
checksums = Checksums.new(@path)
checksums.annotate(@metadata)
modulefile_path = File.join(@path, 'Modulefile')
if File.file?(modulefile_path)
Puppet::Module::Tool::ModulefileReader.evaluate(@metadata, modulefile_path)
elsif require_modulefile
raise ArgumentError, "No Modulefile found."
end
end
@metadata
end
def load_modulefile!
@metadata = nil
metadata(true)
end
- # Use to extract and validate a module name and version from a
- # filename
- # Note: Must have @filename set to use this
- def parse_filename!
- @release_name = File.basename(@filename,'.tar.gz')
- match = /^(.*?)-(.*?)-(\d+\.\d+\.\d+.*?)$/.match(@release_name)
- if match then
- @username, @module_name, @version = match.captures
+ def parse_filename(filename)
+ if match = /^((.*?)-(.*?))-(\d+\.\d+\.\d+.*?)$/.match(File.basename(filename,'.tar.gz'))
+ module_name, author, shortname, version = match.captures
else
raise ArgumentError, "Could not parse filename to obtain the username, module name and version. (#{@release_name})"
end
- @full_module_name = [@username, @module_name].join('-')
- unless @username && @module_name
- raise ArgumentError, "Username and Module name not provided"
- end
- unless SemVer.valid?(@version)
- raise ArgumentError, "Invalid version format: #{@version} (Semantic Versions are acceptable: http://semver.org)"
+
+ unless SemVer.valid?(version)
+ raise ArgumentError, "Invalid version format: #{version} (Semantic Versions are acceptable: http://semver.org)"
end
+
+ return {
+ :module_name => module_name,
+ :author => author,
+ :dir_name => shortname,
+ :version => version
+ }
end
end
end
end
diff --git a/lib/puppet/module_tool/applications/checksummer.rb b/lib/puppet/module_tool/applications/checksummer.rb
index 2ea1ef587..f0c3a7130 100644
--- a/lib/puppet/module_tool/applications/checksummer.rb
+++ b/lib/puppet/module_tool/applications/checksummer.rb
@@ -1,47 +1,56 @@
+require 'puppet/module_tool/checksums'
+
module Puppet::Module::Tool
module Applications
class Checksummer < Application
def initialize(path, options = {})
@path = Pathname.new(path)
super(options)
end
def run
changes = []
if metadata_file.exist?
- sums = Checksums.new(@path)
+ sums = Puppet::Module::Tool::Checksums.new(@path)
(metadata['checksums'] || {}).each do |child_path, canonical_checksum|
+
+ # Work around an issue where modules built with an older version
+ # of PMT would include the metadata.json file in the list of files
+ # checksummed. This causes metadata.json to always report local
+ # changes.
+ next if File.basename(child_path) == "metadata.json"
+
path = @path + child_path
if canonical_checksum != sums.checksum(path)
changes << child_path
end
end
else
raise ArgumentError, "No metadata.json found."
end
# Return an Array of strings representing file paths of files that have
# been modified since this module was installed. All paths are relative
# to the installed module directory. This return value is used by the
# module_tool face changes action, and displayed on the console.
#
# Example return value:
#
- # [ "REVISION", "metadata.json", "manifests/init.pp"]
+ # [ "REVISION", "manifests/init.pp"]
#
changes
end
private
def metadata
PSON.parse(metadata_file.read)
end
def metadata_file
(@path + 'metadata.json')
end
end
end
end
diff --git a/lib/puppet/module_tool/applications/cleaner.rb b/lib/puppet/module_tool/applications/cleaner.rb
deleted file mode 100644
index b811983d7..000000000
--- a/lib/puppet/module_tool/applications/cleaner.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-module Puppet::Module::Tool
- module Applications
- class Cleaner < Application
- def run
- Puppet::Forge::Cache.clean
-
- # Return a status Hash containing the status of the clean command
- # and a status message. This return value is used by the module_tool
- # face clean action, and the status message, return_value[:msg], is
- # displayed on the console.
- #
- { :status => "success", :msg => "Cleaned module cache." }
- end
- end
- end
-end
diff --git a/lib/puppet/module_tool/applications/installer.rb b/lib/puppet/module_tool/applications/installer.rb
index d76e0e308..78614930c 100644
--- a/lib/puppet/module_tool/applications/installer.rb
+++ b/lib/puppet/module_tool/applications/installer.rb
@@ -1,54 +1,183 @@
require 'open-uri'
require 'pathname'
require 'tmpdir'
+require 'semver'
+require 'puppet/forge'
+require 'puppet/module_tool'
+require 'puppet/module_tool/shared_behaviors'
module Puppet::Module::Tool
module Applications
class Installer < Application
+ include Puppet::Module::Tool::Errors
+
def initialize(name, options = {})
- @forge = Puppet::Forge::Forge.new
- @install_params = {}
+ @action = :install
+ @environment = Puppet::Node::Environment.new(Puppet.settings[:environment])
+ @force = options[:force]
+ @ignore_dependencies = options[:force] || options[:ignore_dependencies]
+ @name = name
+ super(options)
+ end
+
+ def run
+ begin
+ if is_module_package?(@name)
+ @source = :filesystem
+ @filename = File.expand_path(@name)
+ raise MissingPackageError, :requested_package => @filename unless File.exist?(@filename)
- if File.exist?(name)
- if File.directory?(name)
- # TODO Unify this handling with that of Unpacker#check_clobber!
- raise ArgumentError, "Module already installed: #{name}"
+ parsed = parse_filename(@filename)
+ @module_name = parsed[:module_name]
+ @version = parsed[:version]
+ else
+ @source = :repository
+ @module_name = @name.gsub('/', '-')
+ @version = options[:version]
end
- @filename = File.expand_path(name)
- @install_params[:source] = :filesystem
- @install_params[:filename] = @filename
- parse_filename!
- else
- @install_params[:source] = :repository
- begin
- @install_params[:author], @install_params[:modname] = Puppet::Module::Tool::username_and_modname_from(name)
- rescue ArgumentError
- raise "Could not install module with invalid name: #{name}"
+
+ results = {
+ :module_name => @module_name,
+ :module_version => @version,
+ :install_dir => options[:target_dir],
+ }
+
+ unless File.directory? options[:target_dir]
+ raise MissingInstallDirectoryError,
+ :requested_module => @module_name,
+ :requested_version => @version || 'latest',
+ :directory => options[:target_dir]
end
- @install_params[:version_requirement] = options[:version]
+
+ cached_paths = get_release_packages
+
+ unless @graph.empty?
+ Puppet.notice 'Installing -- do not interrupt ...'
+ cached_paths.each do |hash|
+ hash.each do |dir, path|
+ Unpacker.new(path, @options.merge(:target_dir => dir)).run
+ end
+ end
+ end
+ rescue ModuleToolError => err
+ results[:error] = {
+ :oneline => err.message,
+ :multiline => err.multiline,
+ }
+ else
+ results[:result] = :success
+ results[:installed_modules] = @graph
+ ensure
+ results[:result] ||= :failure
end
- super(options)
+
+ results
+ end
+
+ private
+
+ include Puppet::Module::Tool::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 !@force && @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 => @installed[@module_name].first.local_changes
+ end
+
+ if @ignore_dependencies && @source == :filesystem
+ @urls = {}
+ @remote = { "#{@module_name}@#{@version}" => { } }
+ @versions = {
+ @module_name => [
+ { :vstring => @version, :semver => SemVer.new(@version) }
+ ]
+ }
+ else
+ get_remote_constraints
+ end
+
+ @graph = resolve_constraints({ @module_name => @version })
+ @graph.first[:tarball] = @filename if @source == :filesystem
+ resolve_install_conflicts(@graph) unless @force
+
+ # 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])
end
- def force?
- options[:force]
+ #
+ # Resolve installation conflicts by checking if the requested module
+ # or one of it's 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)
+ 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
+ latest_version = @versions["#{@module_name}"].sort_by { |h| h[:semver] }.last[:vstring]
+
+ raise InstallConflictError,
+ :requested_module => @module_name,
+ :requested_version => @version || "latest: v#{latest_version}",
+ :dependency => dependency,
+ :directory => mod.path,
+ :metadata => metadata
+ end
+
+ resolve_install_conflicts(release[:dependencies], true)
+ end
+ end
end
- def run
- cache_path = @forge.get_release_package(@install_params)
-
- module_dir = Unpacker.run(cache_path, options)
- # Return the Pathname object representing the path to the installed
- # module. This return value is used by the module_tool face install
- # action, and displayed to on the console.
- #
- # Example return value:
- #
- # "/etc/puppet/modules/apache"
- #
- module_dir
+ #
+ # Check if a file is a vaild module package.
+ # ---
+ # FIXME: Checking for a valid module package should be more robust and
+ # use the acutal 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/searcher.rb b/lib/puppet/module_tool/applications/searcher.rb
index 97028cd44..923aaf92d 100644
--- a/lib/puppet/module_tool/applications/searcher.rb
+++ b/lib/puppet/module_tool/applications/searcher.rb
@@ -1,16 +1,15 @@
module Puppet::Module::Tool
module Applications
class Searcher < Application
def initialize(term, options = {})
@term = term
- @forge = Puppet::Forge::Forge.new
super(options)
end
def run
- @forge.search(@term)
+ Puppet::Forge.search(@term)
end
end
end
end
diff --git a/lib/puppet/module_tool/applications/uninstaller.rb b/lib/puppet/module_tool/applications/uninstaller.rb
index 4769e56ff..2ee9b9818 100644
--- a/lib/puppet/module_tool/applications/uninstaller.rb
+++ b/lib/puppet/module_tool/applications/uninstaller.rb
@@ -1,59 +1,107 @@
module Puppet::Module::Tool
module Applications
class Uninstaller < Application
+ include Puppet::Module::Tool::Errors
def initialize(name, options)
- @name = name
- @options = options
- @errors = Hash.new {|h, k| h[k] = []}
- @removed_mods = []
+ @name = name
+ @options = options
+ @errors = Hash.new {|h, k| h[k] = {}}
+ @unfiltered = []
+ @installed = []
+ @suggestions = []
@environment = Puppet::Node::Environment.new(options[:environment])
end
def run
- if module_installed?
- uninstall
- else
- @errors[@name] << "Module #{@name} is not installed"
+ results = {
+ :module_name => @name,
+ :requested_version => @version,
+ }
+
+ begin
+ find_installed_module
+ validate_module
+ FileUtils.rm_rf(@installed.first.path)
+
+ 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
- { :removed_mods => @removed_mods, :errors => @errors, :options => @options }
+
+ results
end
private
- def version_match?(mod)
- if @options[:version]
- mod.version == @options[:version]
- else
- true
+ 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
- end
- def module_installed?
- @environment.module(@name)
+ 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 has_changes?
- Puppet::Module::Tool::Applications::Checksummer.run(@module.path)
- end
+ def validate_module
+ mod = @installed.first
- def uninstall
- # TODO: #11803 Check for broken dependencies before uninstalling modules.
- @environment.modules_by_path.each do |path, modules|
- modules.each do |mod|
- if mod.name == @name
- unless version_match?(mod)
- @errors[@name] << "Installed version of #{mod.name} (v#{mod.version}) does not match version range"
- end
-
- if @errors[@name].empty?
- FileUtils.rm_rf(mod.path)
- @removed_mods << mod
- end
- end
- end
+ if !@options[:force] && mod.has_metadata? && mod.has_local_changes?
+ raise LocalChangesError,
+ :action => :uninstall,
+ :module_name => (mod.forge_name || mod.name).gsub('/', '-'),
+ :requested_version => @options[:version],
+ :installed_version => mod.version
+ 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/unpacker.rb b/lib/puppet/module_tool/applications/unpacker.rb
index 119eaf323..f06c62d55 100644
--- a/lib/puppet/module_tool/applications/unpacker.rb
+++ b/lib/puppet/module_tool/applications/unpacker.rb
@@ -1,70 +1,48 @@
require 'pathname'
require 'tmpdir'
module Puppet::Module::Tool
module Applications
class Unpacker < Application
def initialize(filename, options = {})
@filename = Pathname.new(filename)
- parse_filename!
+ parsed = parse_filename(filename)
super(options)
- @module_dir = Pathname.new(options[:install_dir]) + @module_name
+ @module_dir = Pathname.new(options[:target_dir]) + parsed[:dir_name]
end
def run
extract_module_to_install_dir
- tag_revision
# Return the Pathname object representing the directory where the
# module release archive was unpacked the to, and the module release
# name.
@module_dir
end
private
-
- def tag_revision
- File.open("#{@module_dir}/REVISION", 'w') do |f|
- f.puts "module: #{@username}/#{@module_name}"
- f.puts "version: #{@version}"
- f.puts "url: file://#{@filename.expand_path}"
- f.puts "installed: #{Time.now}"
- end
- end
-
def extract_module_to_install_dir
delete_existing_installation_or_abort!
build_dir = Puppet::Forge::Cache.base_path + "tmp-unpacker-#{Digest::SHA1.hexdigest(@filename.basename.to_s)}"
build_dir.mkpath
begin
- Puppet.notice "Installing #{@filename.basename} to #{@module_dir.expand_path}"
unless system "tar xzf #{@filename} -C #{build_dir}"
raise RuntimeError, "Could not extract contents of module archive."
end
# grab the first directory
extracted = build_dir.children.detect { |c| c.directory? }
FileUtils.mv extracted, @module_dir
ensure
build_dir.rmtree
end
end
def delete_existing_installation_or_abort!
return unless @module_dir.exist?
-
- if !options[:force]
- Puppet.warning "Existing module '#{@module_dir.expand_path}' found"
- response = prompt "Overwrite module installed at #{@module_dir.expand_path}? [y/N]"
- unless response =~ /y/i
- raise RuntimeError, "Aborted installation."
- end
- end
-
- Puppet.warning "Deleting #{@module_dir.expand_path}"
FileUtils.rm_rf @module_dir
end
end
end
end
diff --git a/lib/puppet/module_tool/applications/upgrader.rb b/lib/puppet/module_tool/applications/upgrader.rb
new file mode 100644
index 000000000..1a56a6deb
--- /dev/null
+++ b/lib/puppet/module_tool/applications/upgrader.rb
@@ -0,0 +1,109 @@
+module Puppet::Module::Tool
+ module Applications
+ class Upgrader < Application
+
+ include Puppet::Module::Tool::Errors
+
+ def initialize(name, options)
+ @action = :upgrade
+ @environment = Puppet::Node::Environment.new(Puppet.settings[:environment])
+ @module_name = name
+ @options = options
+ @force = options[:force]
+ @ignore_dependencies = options[:force] || options[:ignore_dependencies]
+ @version = options[:version]
+ end
+
+ def run
+ begin
+ results = { :module_name => @module_name }
+
+ get_local_constraints
+
+ if @installed[@module_name].length > 1
+ raise MultipleInstalledError,
+ :action => :upgrade,
+ :module_name => @module_name,
+ :installed_modules => @installed[@module_name].sort_by { |mod| @environment.modulepath.index(mod.modulepath) }
+ elsif @installed[@module_name].empty?
+ raise NotInstalledError,
+ :action => :upgrade,
+ :module_name => @module_name
+ end
+
+ @module = @installed[@module_name].last
+ results[:installed_version] = @module.version ? @module.version.sub(/^(?=\d)/, 'v') : nil
+ results[:requested_version] = @version || (@conditions[@module_name].empty? ? :latest : :best)
+ dir = @module.modulepath
+
+ Puppet.notice "Found '#{@module_name}' (#{colorize(:cyan, results[:installed_version] || '???')}) in #{dir} ..."
+ if !@options[:force] && @module.has_metadata? && @module.has_local_changes?
+ raise LocalChangesError,
+ :action => :upgrade,
+ :module_name => @module_name,
+ :requested_version => @version || (@conditions[@module_name].empty? ? :latest : :best),
+ :installed_version => @module.version
+ end
+
+ begin
+ get_remote_constraints
+ rescue => e
+ raise UnknownModuleError, results.merge(:repository => Puppet::Forge.repository.uri)
+ else
+ raise UnknownVersionError, results.merge(:repository => Puppet::Forge.repository.uri) if @remote.empty?
+ end
+
+ if !@options[:force] && @versions["#{@module_name}"].last[:vstring].sub(/^(?=\d)/, 'v') == (@module.version || '0.0.0').sub(/^(?=\d)/, 'v')
+ raise VersionAlreadyInstalledError,
+ :module_name => @module_name,
+ :requested_version => @version || ((@conditions[@module_name].empty? ? 'latest' : 'best') + ": #{@versions["#{@module_name}"].last[:vstring].sub(/^(?=\d)/, 'v')}"),
+ :installed_version => @installed[@module_name].last.version,
+ :conditions => @conditions[@module_name] + [{ :module => :you, :version => @version }]
+ end
+
+ @graph = resolve_constraints({ @module_name => @version })
+
+ # 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
+ tarballs = download_tarballs(@graph, @graph.last[:path])
+
+ unless @graph.empty?
+ Puppet.notice 'Upgrading -- do not interrupt ...'
+ tarballs.each do |hash|
+ hash.each do |dir, path|
+ Unpacker.new(path, @options.merge(:target_dir => dir)).run
+ end
+ end
+ end
+
+ results[:result] = :success
+ results[:base_dir] = @graph.first[:path]
+ results[:affected_modules] = @graph
+ rescue VersionAlreadyInstalledError => e
+ results[:result] = :noop
+ 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
+
+ return results
+ end
+
+ private
+ include Puppet::Module::Tool::Shared
+ end
+ end
+end
diff --git a/lib/puppet/module_tool/errors.rb b/lib/puppet/module_tool/errors.rb
new file mode 100644
index 000000000..b90e5ba97
--- /dev/null
+++ b/lib/puppet/module_tool/errors.rb
@@ -0,0 +1,9 @@
+module Puppet::Module::Tool
+ module Errors
+ require 'puppet/module_tool/errors/base'
+ require 'puppet/module_tool/errors/installer'
+ require 'puppet/module_tool/errors/uninstaller'
+ require 'puppet/module_tool/errors/upgrader'
+ require 'puppet/module_tool/errors/shared'
+ end
+end
diff --git a/lib/puppet/module_tool/errors/base.rb b/lib/puppet/module_tool/errors/base.rb
new file mode 100644
index 000000000..8ec3e7599
--- /dev/null
+++ b/lib/puppet/module_tool/errors/base.rb
@@ -0,0 +1,15 @@
+module Puppet::Module::Tool::Errors
+ class ModuleToolError < StandardError
+ def v(version)
+ (version || '???').to_s.sub(/^(?=\d)/, 'v')
+ end
+
+ def vstring
+ if @action == :upgrade
+ "#{v(@installed_version)} -> #{v(@requested_version)}"
+ else
+ "#{v(@installed_version || @requested_version)}"
+ end
+ end
+ end
+end
diff --git a/lib/puppet/module_tool/errors/installer.rb b/lib/puppet/module_tool/errors/installer.rb
new file mode 100644
index 000000000..48b3b77bf
--- /dev/null
+++ b/lib/puppet/module_tool/errors/installer.rb
@@ -0,0 +1,90 @@
+module Puppet::Module::Tool::Errors
+
+ class InstallError < ModuleToolError; end
+
+ class AlreadyInstalledError < InstallError
+ def initialize(options)
+ @module_name = options[:module_name]
+ @installed_version = v(options[:installed_version])
+ @requested_version = v(options[:requested_version])
+ @local_changes = options[:local_changes]
+ super "'#{@module_name}' (#{@requested_version}) requested; '#{@module_name}' (#{@installed_version}) already installed"
+ end
+
+ def multiline
+ message = []
+ message << "Could not install module '#{@module_name}' (#{@requested_version})"
+ message << " Module '#{@module_name}' (#{@installed_version}) is already installed"
+ message << " Installed module has had changes made locally" unless @local_changes.empty?
+ message << " Use `puppet module upgrade` to install a different version"
+ message << " Use `puppet module install --force` to re-install only this module"
+ message.join("\n")
+ end
+ end
+
+ class InstallConflictError < InstallError
+ 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
+
+ message << " Use `puppet module install --dir ` to install modules elsewhere"
+
+ 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 MissingPackageError < InstallError
+ def initialize(options)
+ @requested_package = options[:requested_package]
+ super "#{@requested_package} requested; Package #{@requested_package} does not exist"
+ end
+
+ def multiline
+ <<-MSG.strip
+Could not install package #{@requested_package}
+ Package #{@requested_package} does not exist
+ MSG
+ end
+ end
+
+ class MissingInstallDirectoryError < InstallError
+ def initialize(options)
+ @requested_module = options[:requested_module]
+ @requested_version = options[:requested_version]
+ @directory = options[:directory]
+ super "'#{@requested_module}' (#{@requested_version}) requested; Directory #{@directory} does not exist"
+ end
+
+ def multiline
+ <<-MSG.strip
+Could not install module '#{@requested_module}' (#{@requested_version})
+ Directory #{@directory} does not exist
+ MSG
+ end
+ end
+end
diff --git a/lib/puppet/module_tool/errors/shared.rb b/lib/puppet/module_tool/errors/shared.rb
new file mode 100644
index 000000000..f24d16bfe
--- /dev/null
+++ b/lib/puppet/module_tool/errors/shared.rb
@@ -0,0 +1,115 @@
+module Puppet::Module::Tool::Errors
+
+ class NoVersionsSatisfyError < ModuleToolError
+ def initialize(options)
+ @requested_name = options[:requested_name]
+ @requested_version = options[:requested_version]
+ @installed_version = options[:installed_version]
+ @dependency_name = options[:dependency_name]
+ @conditions = options[:conditions]
+ @action = options[:action]
+
+ super "Could not #{@action} '#{@requested_name}' (#{vstring}); module '#{@dependency_name}' cannot satisfy dependencies"
+ end
+
+ def multiline
+ same_mod = @requested_name == @dependency_name
+
+ message = []
+ message << "Could not #{@action} module '#{@requested_name}' (#{vstring})"
+ message << " No version of '#{@dependency_name}' will satisfy dependencies"
+ message << " You specified '#{@requested_name}' (#{v(@requested_version)})" if same_mod
+ message += @conditions.select { |c| c[:module] != :you }.sort_by { |c| c[:module] }.map do |c|
+ " '#{c[:module]}' (#{v(c[:version])}) requires '#{@dependency_name}' (#{v(c[:dependency])})"
+ end
+ message << " Use `puppet module #{@action} --force` to #{@action} this module anyway" if same_mod
+ message << " Use `puppet module #{@action} --ignore-dependencies` to #{@action} only this module" unless same_mod
+
+ 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 is not installed"
+ 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.join("\n")
+ end
+ end
+end
diff --git a/lib/puppet/module_tool/errors/uninstaller.rb b/lib/puppet/module_tool/errors/uninstaller.rb
new file mode 100644
index 000000000..3beee7872
--- /dev/null
+++ b/lib/puppet/module_tool/errors/uninstaller.rb
@@ -0,0 +1,45 @@
+module Puppet::Module::Tool::Errors
+
+ class UninstallError < ModuleToolError; end
+
+ class NoVersionMatchesError < UninstallError
+ def initialize(options)
+ @module_name = options[:module_name]
+ @modules = options[:installed_modules]
+ @version = options[:version_range]
+ super "Could not uninstall '#{@module_name}'; no installed version matches"
+ end
+
+ def multiline
+ message = []
+ message << "Could not uninstall module '#{@module_name}' (#{v(@version)})"
+ message << " No installed version of '#{@module_name}' matches (#{v(@version)})"
+ message += @modules.map do |mod|
+ " '#{mod[:name]}' (#{v(mod[:version])}) is installed in #{mod[:path]}"
+ end
+ message.join("\n")
+ end
+ end
+
+ class ModuleIsRequiredError < UninstallError
+ def initialize(options)
+ @module_name = options[:module_name]
+ @required_by = options[:required_by]
+ @requested_version = options[:requested_version]
+ @installed_version = options[:installed_version]
+
+ super "Could not uninstall '#{@module_name}'; installed modules still depend upon it"
+ end
+
+ def multiline
+ message = []
+ message << ("Could not uninstall module '#{@module_name}'" << (@requested_version ? " (#{v(@requested_version)})" : ''))
+ message << " Other installed modules have dependencies on '#{@module_name}' (#{v(@installed_version)})"
+ message += @required_by.map do |mod|
+ " '#{mod['name']}' (#{v(mod['version'])}) requires '#{@module_name}' (#{v(mod['version_requirement'])})"
+ end
+ message << " Use `puppet module uninstall --force` to uninstall this module anyway"
+ message.join("\n")
+ end
+ end
+end
diff --git a/lib/puppet/module_tool/errors/upgrader.rb b/lib/puppet/module_tool/errors/upgrader.rb
new file mode 100644
index 000000000..abc8375b8
--- /dev/null
+++ b/lib/puppet/module_tool/errors/upgrader.rb
@@ -0,0 +1,72 @@
+module Puppet::Module::Tool::Errors
+
+ class UpgradeError < ModuleToolError
+ def initialize(msg)
+ @action = :upgrade
+ super
+ end
+ end
+
+ class VersionAlreadyInstalledError < UpgradeError
+ def initialize(options)
+ @module_name = options[:module_name]
+ @requested_version = options[:requested_version]
+ @installed_version = options[:installed_version]
+ @dependency_name = options[:dependency_name]
+ @conditions = options[:conditions]
+ super "Could not upgrade '#{@module_name}'; module is not installed"
+ end
+
+ def multiline
+ message = []
+ message << "Could not upgrade module '#{@module_name}' (#{vstring})"
+ if @conditions.length == 1 && @conditions.last[:version].nil?
+ message << " The installed version is already the latest version"
+ else
+ message << " The installed version is already the best fit for the current dependencies"
+ message += @conditions.select { |c| c[:module] == :you && c[:version] }.map do |c|
+ " You specified '#{@module_name}' (#{v(c[:version])})"
+ end
+ message += @conditions.select { |c| c[:module] != :you }.sort_by { |c| c[:module] }.map do |c|
+ " '#{c[:module]}' (#{v(c[:version])}) requires '#{@module_name}' (#{v(c[:dependency])})"
+ end
+ end
+ message << " Use `puppet module install --force` to re-install this module"
+ message.join("\n")
+ end
+ end
+
+ class UnknownModuleError < UpgradeError
+ def initialize(options)
+ @module_name = options[:module_name]
+ @installed_version = options[:installed_version]
+ @requested_version = options[:requested_version]
+ @repository = options[:repository]
+ super "Could not upgrade '#{@module_name}'; module is unknown to #{@repository}"
+ end
+
+ def multiline
+ message = []
+ message << "Could not upgrade module '#{@module_name}' (#{vstring})"
+ message << " Module '#{@module_name}' does not exist on #{@repository}"
+ message.join("\n")
+ end
+ end
+
+ class UnknownVersionError < UpgradeError
+ def initialize(options)
+ @module_name = options[:module_name]
+ @installed_version = options[:installed_version]
+ @requested_version = options[:requested_version]
+ @repository = options[:repository]
+ super "Could not upgrade '#{@module_name}' (#{vstring}); module has no versions #{ @requested_version && "matching #{v(@requested_version)} "}published on #{@repository}"
+ end
+
+ def multiline
+ message = []
+ message << "Could not upgrade module '#{@module_name}' (#{vstring})"
+ message << " No version matching '#{@requested_version || ">= 0.0.0"}' exists on #{@repository}"
+ message.join("\n")
+ end
+ end
+end
diff --git a/lib/puppet/module_tool/shared_behaviors.rb b/lib/puppet/module_tool/shared_behaviors.rb
new file mode 100644
index 000000000..dae588e34
--- /dev/null
+++ b/lib/puppet/module_tool/shared_behaviors.rb
@@ -0,0 +1,161 @@
+module Puppet::Module::Tool::Shared
+
+ include Puppet::Module::Tool::Errors
+
+ def get_local_constraints
+ @local = Hash.new { |h,k| h[k] = { } }
+ @conditions = Hash.new { |h,k| h[k] = [] }
+ @installed = Hash.new { |h,k| h[k] = [] }
+
+ @environment.modules_by_path.values.flatten.each do |mod|
+ mod_name = (mod.forge_name || mod.name).gsub('/', '-')
+ @installed[mod_name] << mod
+ d = @local["#{mod_name}@#{mod.version}"]
+ (mod.dependencies || []).each do |hash|
+ name, conditions = hash['name'], hash['version_requirement']
+ name = name.gsub('/', '-')
+ d[name] = conditions
+ @conditions[name] << {
+ :module => mod_name,
+ :version => mod.version,
+ :dependency => conditions
+ }
+ end
+ end
+ end
+
+ def get_remote_constraints
+ @remote = Hash.new { |h,k| h[k] = { } }
+ @urls = {}
+ @versions = Hash.new { |h,k| h[k] = [] }
+
+ Puppet.notice "Downloading from #{Puppet::Forge.repository.uri} ..."
+ author, modname = Puppet::Module::Tool.username_and_modname_from(@module_name)
+ info = Puppet::Forge.remote_dependency_info(author, modname, @options[:version])
+ info.each do |pair|
+ mod_name, releases = pair
+ mod_name = mod_name.gsub('/', '-')
+ releases.each do |rel|
+ semver = SemVer.new(rel['version'] || '0.0.0') rescue SemVer.MIN
+ @versions[mod_name] << { :vstring => rel['version'], :semver => semver }
+ @versions[mod_name].sort! { |a, b| a[:semver] <=> b[:semver] }
+ @urls["#{mod_name}@#{rel['version']}"] = rel['file']
+ d = @remote["#{mod_name}@#{rel['version']}"]
+ (rel['dependencies'] || []).each do |name, conditions|
+ d[name.gsub('/', '-')] = conditions
+ end
+ end
+ end
+ end
+
+ def implicit_version(mod)
+ return :latest if @conditions[mod].empty?
+ if @conditions[mod].all? { |c| c[:queued] || c[:module] == :you }
+ return :latest
+ end
+ return :best
+ end
+
+ def annotated_version(mod, versions)
+ if versions.empty?
+ return implicit_version(mod)
+ else
+ return "#{implicit_version(mod)}: #{versions.last}"
+ end
+ end
+
+ def resolve_constraints(dependencies, source = [{:name => :you}], seen = {}, action = @action)
+ dependencies = dependencies.map do |mod, range|
+ source.last[:dependency] = range
+
+ @conditions[mod] << {
+ :module => source.last[:name],
+ :version => source.last[:version],
+ :dependency => range,
+ :queued => true
+ }
+
+ if @force
+ range = SemVer[@version] rescue SemVer['>= 0.0.0']
+ else
+ range = (@conditions[mod]).map do |r|
+ SemVer[r[:dependency]] rescue SemVer['>= 0.0.0']
+ end.inject(&:&)
+ end
+
+ if @action == :install && seen.include?(mod)
+ next if range === seen[mod][:semver]
+
+ req_module = @module_name
+ req_versions = @versions["#{@module_name}"].map { |v| v[:semver] }
+ raise InvalidDependencyCycleError,
+ :module_name => mod,
+ :source => (source + [{ :name => mod, :version => source.last[:dependency] }]),
+ :requested_module => req_module,
+ :requested_version => @version || annotated_version(req_module, req_versions),
+ :conditions => @conditions
+ end
+
+ if !(@force || @installed[mod].empty? || source.last[:name] == :you)
+ next if range === SemVer.new(@installed[mod].first.version)
+ action = :upgrade
+ elsif @installed[mod].empty?
+ action = :install
+ end
+
+ if action == :upgrade
+ @conditions.each { |_, conds| conds.delete_if { |c| c[:module] == mod } }
+ end
+
+ valid_versions = @versions["#{mod}"].select { |h| range === h[:semver] }
+
+ unless version = valid_versions.last
+ req_module = @module_name
+ req_versions = @versions["#{@module_name}"].map { |v| v[:semver] }
+ raise NoVersionsSatisfyError,
+ :requested_name => req_module,
+ :requested_version => @version || annotated_version(req_module, req_versions),
+ :installed_version => @installed[@module_name].empty? ? nil : @installed[@module_name].first.version,
+ :dependency_name => mod,
+ :conditions => @conditions[mod],
+ :action => @action
+ end
+
+ seen[mod] = version
+
+ {
+ :module => mod,
+ :version => version,
+ :action => action,
+ :previous_version => @installed[mod].empty? ? nil : @installed[mod].first.version,
+ :file => @urls["#{mod}@#{version[:vstring]}"],
+ :path => action == :install ? @options[:target_dir] : (@installed[mod].empty? ? @options[:target_dir] : @installed[mod].first.modulepath),
+ :dependencies => []
+ }
+ end.compact
+ dependencies.each do |mod|
+ deps = @remote["#{mod[:module]}@#{mod[:version][:vstring]}"].sort_by(&:first)
+ mod[:dependencies] = resolve_constraints(deps, source + [{ :name => mod[:module], :version => mod[:version][:vstring] }], seen, :install)
+ end unless @ignore_dependencies
+ return dependencies
+ end
+
+ def download_tarballs(graph, default_path)
+ graph.map do |release|
+ begin
+ if release[:tarball]
+ cache_path = Pathname(release[:tarball])
+ else
+ cache_path = Puppet::Forge.repository.retrieve(release[:file])
+ end
+ rescue OpenURI::HTTPError => e
+ raise RuntimeError, "Could not download module: #{e.message}"
+ end
+
+ [
+ { (release[:path] ||= default_path) => cache_path},
+ *download_tarballs(release[:dependencies], default_path)
+ ]
+ end.flatten
+ end
+end
diff --git a/lib/puppet/module_tool/skeleton/templates/generator/metadata.json b/lib/puppet/module_tool/skeleton/templates/generator/metadata.json
deleted file mode 100644
index 8ce7797ff..000000000
--- a/lib/puppet/module_tool/skeleton/templates/generator/metadata.json
+++ /dev/null
@@ -1,12 +0,0 @@
-/*
-+-----------------------------------------------------------------------+
-| |
-| ==> DO NOT EDIT THIS FILE! <== |
-| |
-| You should edit the `Modulefile` and run `puppet-module build` |
-| to generate the `metadata.json` file for your releases. |
-| |
-+-----------------------------------------------------------------------+
-*/
-
-{}
diff --git a/lib/puppet/module_tool/utils.rb b/lib/puppet/module_tool/utils.rb
deleted file mode 100644
index 85f57c973..000000000
--- a/lib/puppet/module_tool/utils.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-module Puppet::Module::Tool
- module Utils
- require 'puppet/module_tool/utils/interrogation'
- end
-end
diff --git a/lib/puppet/module_tool/utils/interrogation.rb b/lib/puppet/module_tool/utils/interrogation.rb
deleted file mode 100644
index 19450dedd..000000000
--- a/lib/puppet/module_tool/utils/interrogation.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-module Puppet::Module::Tool
- module Utils
-
- # = Interrogation
- #
- # This module contains methods to emit questions to the console.
- module Interrogation
- def confirms?(question)
- $stderr.print "#{question} [y/N]: "
- $stdin.gets =~ /y/i
- end
-
- def prompt(question, quiet = false)
- $stderr.print "#{question}: "
- system 'stty -echo' if quiet
- $stdin.gets.strip
- ensure
- if quiet
- system 'stty echo'
- say "\n---------"
- end
- end
- end
- end
-end
diff --git a/lib/puppet/util/terminal.rb b/lib/puppet/util/terminal.rb
new file mode 100644
index 000000000..ba725702f
--- /dev/null
+++ b/lib/puppet/util/terminal.rb
@@ -0,0 +1,16 @@
+module Puppet::Util::Terminal
+ # Attempts to determine the width of the terminal. This is currently only
+ # supported on POSIX systems, and relies on the claims of `stty` (or `tput`).
+ #
+ # Inspired by code from Thor; thanks wycats!
+ # @return [Number] The column width of the terminal. Defaults to 80 columns.
+ def self.width
+ if Puppet.features.posix?
+ result = %x{stty size 2>/dev/null}.split[1] ||
+ %x{tput cols 2>/dev/null}.split[0]
+ end
+ return (result || '80').to_i
+ rescue
+ return 80
+ end
+end
diff --git a/spec/integration/module_tool_spec.rb b/spec/integration/module_tool_spec.rb
deleted file mode 100644
index bd3b12649..000000000
--- a/spec/integration/module_tool_spec.rb
+++ /dev/null
@@ -1,475 +0,0 @@
-require 'spec_helper'
-require 'tmpdir'
-require 'fileutils'
-
-# FIXME This are helper methods that could be used by other tests in the
-# future, should we move these to a more central location
-def stub_repository_read(code, body)
- kind = Net::HTTPResponse.send(:response_class, code.to_s)
- response = kind.new('1.0', code.to_s, 'HTTP MESSAGE')
- response.stubs(:read_body).returns(body)
- Puppet::Forge::Repository.any_instance.stubs(:read_response).returns(response)
-end
-
-def stub_installer_read(body)
- Puppet::Module::Tool::Applications::Installer.any_instance.stubs(:read_match).returns(body)
-end
-
-def stub_cache_read(body)
- Puppet::Forge::Cache.any_instance.stubs(:read_retrieve).returns(body)
-end
-
-# Return path to temparory directory for testing.
-def testdir
- return @testdir ||= tmpdir("module_tool_testdir")
-end
-
-# Create a temporary testing directory, change into it, and execute the
-# +block+. When the block exists, remove the test directory and change back
-# to the previous directory.
-def mktestdircd(&block)
- previousdir = Dir.pwd
- rmtestdir
- FileUtils.mkdir_p(testdir)
- Dir.chdir(testdir)
- block.call
-ensure
- rmtestdir
- Dir.chdir previousdir
-end
-
-# Remove the temporary test directory.
-def rmtestdir
- FileUtils.rm_rf(testdir) if File.directory?(testdir)
-end
-# END helper methods
-
-
-# Directory that contains sample releases.
-RELEASE_FIXTURES_DIR = File.join(PuppetSpec::FIXTURE_DIR, "releases")
-
-# Return the pathname string to the directory containing the release fixture called +name+.
-def release_fixture(name)
- return File.join(RELEASE_FIXTURES_DIR, name)
-end
-
-# Copy the release fixture called +name+ into the current working directory.
-def install_release_fixture(name)
- release_fixture(name)
- FileUtils.cp_r(release_fixture(name), name)
-end
-
-describe "module_tool", :fails_on_windows => true do
- include PuppetSpec::Files
- before do
- @tmp_confdir = Puppet[:confdir] = tmpdir("module_tool_test_confdir")
- @tmp_vardir = Puppet[:vardir] = tmpdir("module_tool_test_vardir")
- Puppet[:module_repository] = "http://forge.puppetlabs.com"
- @mytmpdir = Pathname.new(tmpdir("module_tool_test"))
- @options = {}
- @options[:install_dir] = @mytmpdir
- @options[:module_repository] = "http://forge.puppetlabs.com"
- end
-
- def build_and_install_module
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- Puppet::Module::Tool::Applications::Builder.run(@full_module_name)
-
- FileUtils.mv("#{@full_module_name}/pkg/#{@release_name}.tar.gz", "#{@release_name}.tar.gz")
- FileUtils.rm_rf(@full_module_name)
-
- Puppet::Module::Tool::Applications::Installer.run("#{@release_name}.tar.gz", @options)
- end
-
- # Return STDOUT and STDERR output generated from +block+ as it's run within a temporary test directory.
- def run(&block)
- mktestdircd do
- block.call
- end
- end
-
- before :all do
- @username = "myuser"
- @module_name = "mymodule"
- @full_module_name = "#{@username}-#{@module_name}"
- @version = "0.0.1"
- @release_name = "#{@full_module_name}-#{@version}"
- end
-
- before :each do
- Puppet.settings.stubs(:parse)
- Puppet::Forge::Cache.clean
- end
-
- after :each do
- Puppet::Forge::Cache.clean
- end
-
- describe "generate" do
- it "should generate a module if given a dashed name" do
- run do
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
-
- File.directory?(@full_module_name).should == true
- modulefile = File.join(@full_module_name, "Modulefile")
- File.file?(modulefile).should == true
- metadata = Puppet::Module::Tool::Metadata.new
- Puppet::Module::Tool::ModulefileReader.evaluate(metadata, modulefile)
- metadata.full_module_name.should == @full_module_name
- metadata.username.should == @username
- metadata.name.should == @module_name
- end
- end
-
- it "should fail if given an undashed name" do
- run do
- lambda { Puppet::Module::Tool::Applications::Generator.run("invalid") }.should raise_error(RuntimeError)
- end
- end
-
- it "should fail if directory already exists" do
- run do
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- lambda { Puppet::Module::Tool::Applications::Generator.run(@full_module_name) }.should raise_error(ArgumentError)
- end
- end
-
- it "should return an array of Pathname objects representing paths of generated files" do
- run do
- return_value = Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- return_value.each do |generated_file|
- generated_file.should be_kind_of(Pathname)
- end
- return_value.should be_kind_of(Array)
- end
- end
- end
-
- describe "build" do
- it "should build a module in a directory" do
- run do
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- Puppet::Module::Tool::Applications::Builder.run(@full_module_name)
-
- File.directory?(File.join(@full_module_name, "pkg", @release_name)).should == true
- File.file?(File.join(@full_module_name, "pkg", @release_name + ".tar.gz")).should == true
- metadata_file = File.join(@full_module_name, "pkg", @release_name, "metadata.json")
- File.file?(metadata_file).should == true
- metadata = PSON.parse(File.read(metadata_file))
- metadata["name"].should == @full_module_name
- metadata["version"].should == @version
- metadata["checksums"].should be_a_kind_of(Hash)
- metadata["dependencies"].should == []
- metadata["types"].should == []
- end
- end
-
- it "should build a module's checksums" do
- run do
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- Puppet::Module::Tool::Applications::Builder.run(@full_module_name)
-
- metadata_file = File.join(@full_module_name, "pkg", @release_name, "metadata.json")
- metadata = PSON.parse(File.read(metadata_file))
- metadata["checksums"].should be_a_kind_of(Hash)
-
- modulefile_path = Pathname.new(File.join(@full_module_name, "Modulefile"))
- metadata["checksums"]["Modulefile"].should == Digest::MD5.hexdigest(modulefile_path.read)
- end
- end
-
- it "should build a module's types and providers" do
- run do
- name = "jamtur01-apache"
- install_release_fixture name
- Puppet::Module::Tool::Applications::Builder.run(name)
-
- metadata_file = File.join(name, "pkg", "#{name}-0.0.1", "metadata.json")
- metadata = PSON.parse(File.read(metadata_file))
-
- metadata["types"].size.should == 1
- type = metadata["types"].first
- type["name"].should == "a2mod"
- type["doc"].should == "Manage Apache 2 modules"
-
-
- type["parameters"].size.should == 1
- type["parameters"].first.tap do |o|
- o["name"].should == "name"
- o["doc"].should == "The name of the module to be managed"
- end
-
- type["properties"].size.should == 1
- type["properties"].first.tap do |o|
- o["name"].should == "ensure"
- o["doc"].should =~ /present.+absent/
- end
-
- type["providers"].size.should == 1
- type["providers"].first.tap do |o|
- o["name"].should == "debian"
- o["doc"].should =~ /Manage Apache 2 modules on Debian-like OSes/
- end
- end
- end
-
- it "should build a module's dependencies" do
- run do
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- modulefile = File.join(@full_module_name, "Modulefile")
-
- dependency1_name = "anotheruser-anothermodule"
- dependency1_requirement = ">= 1.2.3"
- dependency2_name = "someuser-somemodule"
- dependency2_requirement = "4.2"
- dependency2_repository = "http://some.repo"
-
- File.open(modulefile, "a") do |handle|
- handle.puts "dependency '#{dependency1_name}', '#{dependency1_requirement}'"
- handle.puts "dependency '#{dependency2_name}', '#{dependency2_requirement}', '#{dependency2_repository}'"
- end
-
- Puppet::Module::Tool::Applications::Builder.run(@full_module_name)
-
- metadata_file = File.join(@full_module_name, "pkg", "#{@full_module_name}-#{@version}", "metadata.json")
- metadata = PSON.parse(File.read(metadata_file))
-
- metadata['dependencies'].size.should == 2
- metadata['dependencies'].sort_by{|t| t['name']}.tap do |dependencies|
- dependencies[0].tap do |dependency1|
- dependency1['name'].should == dependency1_name
- dependency1['version_requirement'].should == dependency1_requirement
- dependency1['repository'].should be_nil
- end
-
- dependencies[1].tap do |dependency2|
- dependency2['name'].should == dependency2_name
- dependency2['version_requirement'].should == dependency2_requirement
- dependency2['repository'].should == dependency2_repository
- end
- end
- end
- end
-
- it "should rebuild a module in a directory" do
- run do
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- Puppet::Module::Tool::Applications::Builder.run(@full_module_name)
- Puppet::Module::Tool::Applications::Builder.run(@full_module_name)
- end
- end
-
- it "should build a module in the current directory" do
- run do
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- Dir.chdir(@full_module_name)
- Puppet::Module::Tool::Applications::Builder.run(Puppet::Module::Tool.find_module_root(nil))
-
- File.file?(File.join("pkg", @release_name + ".tar.gz")).should == true
- end
- end
-
- it "should fail to build a module without a Modulefile" do
- run do
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- FileUtils.rm(File.join(@full_module_name, "Modulefile"))
-
- lambda { Puppet::Module::Tool::Applications::Builder.run(Puppet::Module::Tool.find_module_root(@full_module_name)) }.should raise_error(ArgumentError)
- end
- end
-
- it "should fail to build a module directory that doesn't exist" do
- run do
- lambda { Puppet::Module::Tool::Applications::Builder.run(Puppet::Module::Tool.find_module_root(@full_module_name)) }.should raise_error(ArgumentError)
- end
- end
-
- it "should fail to build a module in the current directory that's not a module" do
- run do
- lambda { Puppet::Module::Tool::Applications::Builder.run(Puppet::Module::Tool.find_module_root(nil)) }.should raise_error(ArgumentError)
- end
- end
-
- it "should return a Pathname object representing the path to the release archive." do
- run do
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- Puppet::Module::Tool::Applications::Builder.run(@full_module_name).should be_kind_of(Pathname)
- end
- end
- end
-
- describe "search" do
- it "should display matching modules" do
- run do
- stub_repository_read 200, <<-HERE
- [
- {"full_module_name": "cli", "version": "1.0"},
- {"full_module_name": "web", "version": "2.0"}
- ]
- HERE
- Puppet::Module::Tool::Applications::Searcher.run("mymodule", @options).size.should == 2
- end
- end
-
- it "should display no matches" do
- run do
- stub_repository_read 200, "[]"
- Puppet::Module::Tool::Applications::Searcher.run("mymodule", @options).should == []
- end
- end
-
- it "should fail if can't get a connection" do
- run do
- stub_repository_read 500, "OH NOES!!1!"
- lambda { Puppet::Module::Tool::Applications::Searcher.run("mymodule", @options) }.should raise_error(RuntimeError)
- end
- end
-
- it "should return an array of module metadata hashes" do
- run do
- results = <<-HERE
- [
- {"full_module_name": "cli", "version": "1.0"},
- {"full_module_name": "web", "version": "2.0"}
- ]
- HERE
- expected = [
- {"version"=>"1.0", "full_module_name"=>"cli"},
- {"version"=>"2.0", "full_module_name"=>"web"}
- ]
- stub_repository_read 200, results
- return_value = Puppet::Module::Tool::Applications::Searcher.run("mymodule", @options)
- return_value.should == expected
- return_value.should be_kind_of(Array)
- end
- end
- end
-
- describe "install" do
- it "should install a module to the puppet modulepath by default" do
- myothertmpdir = Pathname.new(tmpdir("module_tool_test_myothertmpdir"))
- run do
- @options[:install_dir] = myothertmpdir
- Puppet::Module::Tool.unstub(:install_dir)
-
- build_and_install_module
-
- File.directory?(myothertmpdir + @module_name).should == true
- File.file?(myothertmpdir + @module_name + 'metadata.json').should == true
- end
- end
-
- it "should install a module from a filesystem path" do
- run do
- build_and_install_module
-
- File.directory?(@mytmpdir + @module_name).should == true
- File.file?(@mytmpdir + @module_name + 'metadata.json').should == true
- end
- end
-
- it "should install a module from a webserver URL" do
- run do
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- Puppet::Module::Tool::Applications::Builder.run(@full_module_name)
-
- stub_cache_read File.read("#{@full_module_name}/pkg/#{@release_name}.tar.gz")
- FileUtils.rm_rf(@full_module_name)
-
- release = {"file" => "/foo/bar/#{@release_name}.tar.gz", "version" => "#{@version}"}
- Puppet::Forge::Forge.any_instance.stubs(:get_release).returns(release)
-
- Puppet::Module::Tool::Applications::Installer.run(@full_module_name, @options)
-
- File.directory?(@mytmpdir + @module_name).should == true
- File.file?(@mytmpdir + @module_name + 'metadata.json').should == true
- end
- end
-
- it "should install a module from a webserver URL using a version requirement" # TODO
-
- it "should fail if module isn't a slashed name" do
- run do
- lambda { Puppet::Module::Tool::Applications::Installer.run("invalid") }.should raise_error(RuntimeError)
- end
- end
-
- it "should fail if module doesn't exist on webserver" do
- run do
- stub_installer_read "{}"
- lambda { Puppet::Module::Tool::Applications::Installer.run("not-found", @options) }.should raise_error(RuntimeError)
- end
- end
-
- it "should fail gracefully when receiving invalid PSON" do
- pending "Implement PSON error wrapper" # TODO
- run do
- stub_installer_read "1/0"
- lambda { Puppet::Module::Tool::Applications::Installer.run("not-found") }.should raise_error(SystemExit)
- end
- end
-
- it "should fail if installing a module that's already installed" do
- run do
- name = "myuser-mymodule"
- Dir.mkdir name
- lambda { Puppet::Module::Tool::Applications::Installer.run(name) }.should raise_error(ArgumentError)
- end
- end
-
- it "should return a Pathname object representing the path to the installed module" do
- run do
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- Puppet::Module::Tool::Applications::Builder.run(@full_module_name)
-
- stub_cache_read File.read("#{@full_module_name}/pkg/#{@release_name}.tar.gz")
- FileUtils.rm_rf(@full_module_name)
-
- release = {"file" => "/foo/bar/#{@release_name}.tar.gz", "version" => "#{@version}"}
- Puppet::Forge::Forge.any_instance.stubs(:get_release).returns(release)
-
- Puppet::Module::Tool::Applications::Installer.run(@full_module_name, @options).should be_kind_of(Pathname)
- end
- end
-
- end
-
- describe "clean" do
- require 'puppet/module_tool'
- it "should clean cache" do
- run do
- build_and_install_module
- Puppet::Forge::Cache.base_path.directory?.should == true
- Puppet::Module::Tool::Applications::Cleaner.run
- Puppet::Forge::Cache.base_path.directory?.should == false
- end
- end
-
- it "should return a status Hash" do
- run do
- build_and_install_module
- return_value = Puppet::Module::Tool::Applications::Cleaner.run
- return_value.should include(:msg)
- return_value.should include(:status)
- return_value.should be_kind_of(Hash)
- end
- end
- end
-
- describe "changes" do
- it "should return an array of modified files" do
- run do
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- Puppet::Module::Tool::Applications::Builder.run(@full_module_name)
- Dir.chdir("#{@full_module_name}/pkg/#{@release_name}")
- File.open("Modulefile", "a") do |handle|
- handle.puts
- handle.puts "# Added"
- end
- return_value = Puppet::Module::Tool::Applications::Checksummer.run(".")
- return_value.should include("Modulefile")
- return_value.should be_kind_of(Array)
- end
- end
- end
-end
diff --git a/spec/unit/face/module/install_spec.rb b/spec/unit/face/module/install_spec.rb
new file mode 100644
index 000000000..9f67800a4
--- /dev/null
+++ b/spec/unit/face/module/install_spec.rb
@@ -0,0 +1,158 @@
+require 'spec_helper'
+require 'puppet/face'
+require 'puppet/module_tool'
+
+describe "puppet module install" do
+
+ subject { Puppet::Face[:module, :current] }
+
+ let(:options) do
+ {}
+ end
+
+ describe "option validation" do
+ before do
+ Puppet.settings[:modulepath] = fakemodpath
+ end
+
+ let(:expected_options) do
+ {
+ :target_dir => fakefirstpath,
+ :modulepath => fakemodpath,
+ :environment => 'production'
+ }
+ end
+
+ let(:sep) { File::PATH_SEPARATOR }
+ let(:fakefirstpath) { "/my/fake/modpath" }
+ let(:fakesecondpath) { "/other/fake/path" }
+ let(:fakemodpath) { "#{fakefirstpath}#{sep}#{fakesecondpath}" }
+ let(:fakedirpath) { "/my/fake/path" }
+
+ context "without any options" do
+ it "should require a name" do
+ pattern = /wrong number of arguments/
+ expect { subject.install }.to raise_error ArgumentError, pattern
+ end
+
+ it "should not require any options" do
+ Puppet::Module::Tool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once
+ subject.install("puppetlabs-apache")
+ end
+ end
+
+ it "should accept the --force option" do
+ options[:force] = true
+ expected_options.merge!(options)
+ Puppet::Module::Tool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once
+ subject.install("puppetlabs-apache", options)
+ end
+
+ it "should accept the --target-dir option" do
+ options[:target_dir] = "/foo/puppet/modules"
+ expected_options.merge!(options)
+ expected_options[:modulepath] = "#{options[:target_dir]}#{sep}#{fakemodpath}"
+
+ Puppet::Module::Tool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once
+ subject.install("puppetlabs-apache", options)
+ end
+
+ it "should accept the --version option" do
+ options[:version] = "0.0.1"
+ expected_options.merge!(options)
+ Puppet::Module::Tool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once
+ subject.install("puppetlabs-apache", options)
+ end
+
+ it "should accept the --ignore-dependencies option" do
+ options[:ignore_dependencies] = true
+ expected_options.merge!(options)
+ Puppet::Module::Tool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once
+ subject.install("puppetlabs-apache", options)
+ end
+
+ describe "when modulepath option is passed" do
+ let(:expected_options) { { :modulepath => fakemodpath, :environment => 'production' } }
+ let(:options) { { :modulepath => fakemodpath } }
+
+ describe "when target-dir option is not passed" do
+ it "should set target-dir to be first path from modulepath" do
+ expected_options[:target_dir] = fakefirstpath
+
+ Puppet::Module::Tool::Applications::Installer.
+ expects(:run).
+ with("puppetlabs-apache", expected_options)
+
+ Puppet::Face[:module, :current].install("puppetlabs-apache", options)
+
+ Puppet.settings[:modulepath].should == fakemodpath
+ end
+ end
+
+ describe "when target-dir option is passed" do
+ it "should set target-dir to be first path of modulepath" do
+ options[:target_dir] = fakedirpath
+ expected_options[:target_dir] = fakedirpath
+ expected_options[:modulepath] = "#{fakedirpath}#{sep}#{fakemodpath}"
+
+ Puppet::Module::Tool::Applications::Installer.
+ expects(:run).
+ with("puppetlabs-apache", expected_options)
+
+ Puppet::Face[:module, :current].install("puppetlabs-apache", options)
+
+ Puppet.settings[:modulepath].should == "#{fakedirpath}#{sep}#{fakemodpath}"
+ end
+ end
+ end
+
+ describe "when modulepath option is not passed" do
+ before do
+ Puppet.settings[:modulepath] = fakemodpath
+ end
+
+ describe "when target-dir option is not passed" do
+ it "should set target-dir to be first path of default mod path" do
+ expected_options[:target_dir] = fakefirstpath
+ expected_options[:modulepath] = fakemodpath
+
+ Puppet::Module::Tool::Applications::Installer.
+ expects(:run).
+ with("puppetlabs-apache", expected_options)
+
+ Puppet::Face[:module, :current].install("puppetlabs-apache", options)
+ end
+ end
+
+ describe "when target-dir option is passed" do
+ it "should prepend target-dir to modulepath" do
+ options[:target_dir] = fakedirpath
+ expected_options[:target_dir] = fakedirpath
+ expected_options[:modulepath] = "#{options[:target_dir]}#{sep}#{fakemodpath}"
+
+ Puppet::Module::Tool::Applications::Installer.
+ expects(:run).
+ with("puppetlabs-apache", expected_options)
+
+ Puppet::Face[:module, :current].install("puppetlabs-apache", options)
+ Puppet.settings[:modulepath].should == expected_options[:modulepath]
+ end
+ end
+ end
+ end
+
+ describe "inline documentation" do
+ subject { Puppet::Face[:module, :current].get_action :install }
+
+ its(:summary) { should =~ /install.*module/im }
+ its(:description) { should =~ /install.*module/im }
+ its(:returns) { should =~ /pathname/i }
+ its(:examples) { should_not be_empty }
+
+ %w{ license copyright summary description returns examples }.each do |doc|
+ context "of the" do
+ its(doc.to_sym) { should_not =~ /(FIXME|REVISIT|TODO)/ }
+ end
+ end
+ end
+end
diff --git a/spec/unit/face/module/list_spec.rb b/spec/unit/face/module/list_spec.rb
new file mode 100644
index 000000000..fa1636a4b
--- /dev/null
+++ b/spec/unit/face/module/list_spec.rb
@@ -0,0 +1,182 @@
+# encoding: UTF-8
+
+require 'spec_helper'
+require 'puppet/face'
+require 'puppet/module_tool'
+require 'puppet_spec/modules'
+
+describe "puppet module list", :fails_on_windows => true do
+ include PuppetSpec::Files
+
+ before do
+ dir = tmpdir("deep_path")
+
+ @modpath1 = File.join(dir, "modpath1")
+ @modpath2 = File.join(dir, "modpath2")
+ @modulepath = "#{@modpath1}#{File::PATH_SEPARATOR}#{@modpath2}"
+ Puppet.settings[:modulepath] = @modulepath
+
+ FileUtils.mkdir_p(@modpath1)
+ FileUtils.mkdir_p(@modpath2)
+ end
+
+ it "should return an empty list per dir in path if there are no modules" do
+ Puppet.settings[:modulepath] = @modulepath
+ Puppet::Face[:module, :current].list.should == {
+ @modpath1 => [],
+ @modpath2 => []
+ }
+ end
+
+ it "should include modules separated by the environment's modulepath" do
+ foomod1 = PuppetSpec::Modules.create('foo', @modpath1)
+ barmod1 = PuppetSpec::Modules.create('bar', @modpath1)
+ foomod2 = PuppetSpec::Modules.create('foo', @modpath2)
+
+ env = Puppet::Node::Environment.new
+
+ Puppet::Face[:module, :current].list.should == {
+ @modpath1 => [
+ Puppet::Module.new('bar', :environment => env, :path => barmod1.path),
+ Puppet::Module.new('foo', :environment => env, :path => foomod1.path)
+ ],
+ @modpath2 => [Puppet::Module.new('foo', :environment => env, :path => foomod2.path)]
+ }
+ end
+
+ it "should use the specified environment" do
+ PuppetSpec::Modules.create('foo', @modpath1)
+ PuppetSpec::Modules.create('bar', @modpath1)
+
+ usedenv = Puppet::Node::Environment.new('useme')
+ usedenv.modulepath = [@modpath1, @modpath2]
+
+ Puppet::Face[:module, :current].list(:environment => 'useme').should == {
+ @modpath1 => [
+ Puppet::Module.new('bar', :environment => usedenv),
+ Puppet::Module.new('foo', :environment => usedenv)
+ ],
+ @modpath2 => []
+ }
+ end
+
+ it "should use the specified modulepath" do
+ PuppetSpec::Modules.create('foo', @modpath1)
+ PuppetSpec::Modules.create('bar', @modpath2)
+
+ Puppet::Face[:module, :current].list(:modulepath => "#{@modpath1}#{File::PATH_SEPARATOR}#{@modpath2}").should == {
+ @modpath1 => [ Puppet::Module.new('foo') ],
+ @modpath2 => [ Puppet::Module.new('bar') ]
+ }
+ end
+
+ it "should use the specified modulepath over the specified environment in place of the environment's default path" do
+ foomod1 = PuppetSpec::Modules.create('foo', @modpath1)
+ barmod2 = PuppetSpec::Modules.create('bar', @modpath2)
+ env = Puppet::Node::Environment.new('myenv')
+ env.modulepath = ['/tmp/notused']
+
+ list = Puppet::Face[:module, :current].list(:environment => 'myenv', :modulepath => "#{@modpath1}#{File::PATH_SEPARATOR}#{@modpath2}")
+
+ # Changing Puppet[:modulepath] causes Puppet::Node::Environment.new('myenv')
+ # to have a different object_id than the env above
+ env = Puppet::Node::Environment.new('myenv')
+ list.should == {
+ @modpath1 => [ Puppet::Module.new('foo', :environment => env, :path => foomod1.path) ],
+ @modpath2 => [ Puppet::Module.new('bar', :environment => env, :path => barmod2.path) ]
+ }
+ end
+
+ describe "inline documentation" do
+ subject { Puppet::Face[:module, :current].get_action :list }
+
+ its(:summary) { should =~ /list.*module/im }
+ its(:description) { should =~ /list.*module/im }
+ its(:returns) { should =~ /hash of paths to module objects/i }
+ its(:examples) { should_not be_empty }
+ end
+
+ describe "when rendering" do
+ it "should explicitly state when a modulepath is empty" do
+ empty_modpath = tmpdir('empty')
+ Puppet::Face[:module, :current].list_when_rendering_console(
+ { empty_modpath => [] },
+ {:modulepath => empty_modpath}
+ ).should == <<-HEREDOC.gsub(' ', '')
+ #{empty_modpath} (no modules installed)
+ HEREDOC
+ end
+
+ it "should print both modules with and without metadata" do
+ modpath = tmpdir('modpath')
+ Puppet.settings[:modulepath] = modpath
+ PuppetSpec::Modules.create('nometadata', modpath)
+ PuppetSpec::Modules.create('metadata', modpath, :metadata => {:author => 'metaman'})
+
+ dependency_tree = Puppet::Face[:module, :current].list
+
+ output = Puppet::Face[:module, :current].list_when_rendering_console(
+ dependency_tree,
+ {}
+ )
+
+ output.should == <<-HEREDOC.gsub(' ', '')
+ #{modpath}
+ ├── metaman-metadata (\e[0;36mv9.9.9\e[0m)
+ └── nometadata (\e[0;36m???\e[0m)
+ HEREDOC
+ end
+
+ it "should print the modulepaths in the order they are in the modulepath setting" do
+ path1 = tmpdir('b')
+ path2 = tmpdir('c')
+ path3 = tmpdir('a')
+
+ sep = File::PATH_SEPARATOR
+ Puppet.settings[:modulepath] = "#{path1}#{sep}#{path2}#{sep}#{path3}"
+
+ Puppet::Face[:module, :current].list_when_rendering_console(
+ {
+ path2 => [],
+ path3 => [],
+ path1 => [],
+ },
+ {}
+ ).should == <<-HEREDOC.gsub(' ', '')
+ #{path1} (no modules installed)
+ #{path2} (no modules installed)
+ #{path3} (no modules installed)
+ HEREDOC
+ end
+
+ it "should print dependencies as a tree" do
+ PuppetSpec::Modules.create('dependable', @modpath1, :metadata => { :version => '0.0.5'})
+ PuppetSpec::Modules.create(
+ 'other_mod',
+ @modpath1,
+ :metadata => {
+ :version => '1.0.0',
+ :dependencies => [{
+ "version_requirement" => ">= 0.0.5",
+ "name" => "puppetlabs/dependable"
+ }]
+ }
+ )
+
+ dependency_tree = Puppet::Face[:module, :current].list
+
+ output = Puppet::Face[:module, :current].list_when_rendering_console(
+ dependency_tree,
+ {:tree => true}
+ )
+
+ output.should == <<-HEREDOC.gsub(' ', '')
+ #{@modpath1}
+ └─┬ puppetlabs-other_mod (\e[0;36mv1.0.0\e[0m)
+ └── puppetlabs-dependable (\e[0;36mv0.0.5\e[0m)
+ #{@modpath2} (no modules installed)
+ HEREDOC
+ end
+ end
+
+end
diff --git a/spec/unit/face/module/search_spec.rb b/spec/unit/face/module/search_spec.rb
new file mode 100644
index 000000000..51f62bd1f
--- /dev/null
+++ b/spec/unit/face/module/search_spec.rb
@@ -0,0 +1,163 @@
+require 'spec_helper'
+require 'puppet/face'
+require 'puppet/application/module'
+require 'puppet/module_tool'
+
+describe "puppet module search", :fails_on_windows => true do
+ subject { Puppet::Face[:module, :current] }
+
+ let(:options) do
+ {}
+ end
+
+ describe Puppet::Application::Module do
+ subject do
+ app = Puppet::Application::Module.new
+ app.stubs(:action).returns(Puppet::Face.find_action(:module, :search))
+ app
+ end
+
+ before { subject.render_as = :console }
+ before { Puppet::Util::Terminal.stubs(:width).returns(100) }
+
+ it 'should output nothing when receiving an empty dataset' do
+ subject.render([], ['apache', {}]).should == "No results found for 'apache'."
+ end
+
+ it 'should output a header when receiving a non-empty dataset' do
+ results = [
+ {'full_name' => '', 'author' => '', 'desc' => '', 'tag_list' => [] },
+ ]
+
+ subject.render(results, ['apache', {}]).should =~ /NAME/
+ subject.render(results, ['apache', {}]).should =~ /DESCRIPTION/
+ subject.render(results, ['apache', {}]).should =~ /AUTHOR/
+ subject.render(results, ['apache', {}]).should =~ /KEYWORDS/
+ end
+
+ it 'should output the relevant fields when receiving a non-empty dataset' do
+ results = [
+ {'full_name' => 'Name', 'author' => 'Author', 'desc' => 'Summary', 'tag_list' => ['tag1', 'tag2'] },
+ ]
+
+ subject.render(results, ['apache', {}]).should =~ /Name/
+ subject.render(results, ['apache', {}]).should =~ /Author/
+ subject.render(results, ['apache', {}]).should =~ /Summary/
+ subject.render(results, ['apache', {}]).should =~ /tag1/
+ subject.render(results, ['apache', {}]).should =~ /tag2/
+ end
+
+ it 'should elide really long descriptions' do
+ results = [
+ {
+ 'full_name' => 'Name',
+ 'author' => 'Author',
+ 'desc' => 'This description is really too long to fit in a single data table, guys -- we should probably set about truncating it',
+ 'tag_list' => ['tag1', 'tag2'],
+ },
+ ]
+
+ subject.render(results, ['apache', {}]).should =~ /\.{3} @Author/
+ end
+
+ it 'should never truncate the module name' do
+ results = [
+ {
+ 'full_name' => 'This-module-has-a-really-really-long-name',
+ 'author' => 'Author',
+ 'desc' => 'Description',
+ 'tag_list' => ['tag1', 'tag2'],
+ },
+ ]
+
+ subject.render(results, ['apache', {}]).should =~ /This-module-has-a-really-really-long-name/
+ end
+
+ it 'should never truncate the author name' do
+ results = [
+ {
+ 'full_name' => 'Name',
+ 'author' => 'This-author-has-a-really-really-long-name',
+ 'desc' => 'Description',
+ 'tag_list' => ['tag1', 'tag2'],
+ },
+ ]
+
+ subject.render(results, ['apache', {}]).should =~ /@This-author-has-a-really-really-long-name/
+ end
+
+ it 'should never remove tags that match the search term' do
+ results = [
+ {
+ 'full_name' => 'Name',
+ 'author' => 'Author',
+ 'desc' => 'Description',
+ 'tag_list' => ['Supercalifragilisticexpialidocious'] + (1..100).map { |i| "tag#{i}" },
+ },
+ ]
+
+ subject.render(results, ['Supercalifragilisticexpialidocious', {}]).should =~ /Supercalifragilisticexpialidocious/
+ subject.render(results, ['Supercalifragilisticexpialidocious', {}]).should_not =~ /tag/
+ end
+
+ {
+ 100 => "NAME DESCRIPTION AUTHOR KEYWORDS#{' '*15}\n"\
+ "Name This description is really too long to fit ... @JohnnyApples tag1 tag2 taggitty3#{' '*4}\n",
+
+ 70 => "NAME DESCRIPTION AUTHOR KEYWORDS#{' '*5}\n"\
+ "Name This description is rea... @JohnnyApples tag1 tag2#{' '*4}\n",
+
+ 80 => "NAME DESCRIPTION AUTHOR KEYWORDS#{' '*8}\n"\
+ "Name This description is really too... @JohnnyApples tag1 tag2#{' '*7}\n",
+
+ 200 => "NAME DESCRIPTION AUTHOR KEYWORDS#{' '*48}\n"\
+ "Name This description is really too long to fit in a single data table, guys -- we should probably set about trunca... @JohnnyApples tag1 tag2 taggitty3#{' '*37}\n"
+ }.each do |width, expectation|
+ it "should resize the table to fit the screen, when #{width} columns" do
+ results = [
+ {
+ 'full_name' => 'Name',
+ 'author' => 'JohnnyApples',
+ 'desc' => 'This description is really too long to fit in a single data table, guys -- we should probably set about truncating it',
+ 'tag_list' => ['tag1', 'tag2', 'taggitty3'],
+ },
+ ]
+
+ Puppet::Util::Terminal.expects(:width).returns(width)
+ result = subject.render(results, ['apache', {}])
+ result.lines.sort_by(&:length).last.chomp.length.should <= width
+ result.should == expectation
+ end
+ end
+ end
+
+ describe "option validation" do
+ context "without any options" do
+ it "should require a search term" do
+ pattern = /wrong number of arguments/
+ expect { subject.search }.to raise_error ArgumentError, pattern
+ end
+ end
+
+ it "should accept the --module-repository option" do
+ options[:module_repository] = "http://forge.example.com"
+ Puppet::Module::Tool::Applications::Searcher.expects(:run).with("puppetlabs-apache", options).once
+ subject.search("puppetlabs-apache", options)
+ end
+ end
+
+ describe "inline documentation" do
+ subject { Puppet::Face[:module, :current].get_action :search }
+
+ its(:summary) { should =~ /search.*module/im }
+ its(:description) { should =~ /search.*module/im }
+ its(:returns) { should =~ /array/i }
+ its(:examples) { should_not be_empty }
+
+ %w{ license copyright summary description returns examples }.each do |doc|
+ context "of the" do
+ its(doc.to_sym) { should_not =~ /(FIXME|REVISIT|TODO)/ }
+ end
+ end
+ end
+end
diff --git a/spec/unit/face/module/uninstall_spec.rb b/spec/unit/face/module/uninstall_spec.rb
new file mode 100644
index 000000000..a157df509
--- /dev/null
+++ b/spec/unit/face/module/uninstall_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+require 'puppet/face'
+require 'puppet/module_tool'
+
+describe "puppet module uninstall" do
+ subject { Puppet::Face[:module, :current] }
+
+ let(:options) do
+ {}
+ end
+
+ describe "option validation" do
+ context "without any options" do
+ it "should require a name" do
+ pattern = /wrong number of arguments/
+ expect { subject.uninstall }.to raise_error ArgumentError, pattern
+ end
+
+ it "should not require any options" do
+ Puppet::Module::Tool::Applications::Uninstaller.expects(:run).once
+ subject.uninstall("puppetlabs-apache")
+ end
+ end
+
+ it "should accept the --environment option" do
+ options[:environment] = "development"
+ expected_options = { :environment => 'development' }
+ Puppet::Module::Tool::Applications::Uninstaller.expects(:run).with("puppetlabs-apache", expected_options).once
+ subject.uninstall("puppetlabs-apache", options)
+ end
+
+ it "should accept the --modulepath option" do
+ options[:modulepath] = "/foo/puppet/modules"
+ expected_options = {
+ :modulepath => '/foo/puppet/modules',
+ :environment => 'production',
+ }
+ Puppet::Module::Tool::Applications::Uninstaller.expects(:run).with("puppetlabs-apache", expected_options).once
+ subject.uninstall("puppetlabs-apache", options)
+ end
+
+ it "should accept the --version option" do
+ options[:version] = "1.0.0"
+ expected_options = {
+ :version => '1.0.0',
+ :environment => 'production',
+ }
+ Puppet::Module::Tool::Applications::Uninstaller.expects(:run).with("puppetlabs-apache", expected_options).once
+ subject.uninstall("puppetlabs-apache", options)
+ end
+
+ it "should accept the --force flag" do
+ options[:force] = true
+ expected_options = {
+ :environment => 'production',
+ :force => true,
+ }
+ Puppet::Module::Tool::Applications::Uninstaller.expects(:run).with("puppetlabs-apache", expected_options).once
+ subject.uninstall("puppetlabs-apache", options)
+ end
+ end
+
+ describe "inline documentation" do
+ subject { Puppet::Face[:module, :current].get_action :uninstall }
+
+ its(:summary) { should =~ /uninstall.*module/im }
+ its(:description) { should =~ /uninstall.*module/im }
+ its(:returns) { should =~ /hash of module objects.*/im }
+ its(:examples) { should_not be_empty }
+
+ %w{ license copyright summary description returns examples }.each do |doc|
+ context "of the" do
+ its(doc.to_sym) { should_not =~ /(FIXME|REVISIT|TODO)/ }
+ end
+ end
+ end
+end
diff --git a/spec/unit/face/module/upgrade_spec.rb b/spec/unit/face/module/upgrade_spec.rb
new file mode 100644
index 000000000..c7c2bbcef
--- /dev/null
+++ b/spec/unit/face/module/upgrade_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+require 'puppet/face'
+require 'puppet/module_tool'
+
+describe "puppet module upgrade" do
+ subject { Puppet::Face[:module, :current] }
+
+ let(:options) do
+ {}
+ end
+
+ describe "inline documentation" do
+ subject { Puppet::Face[:module, :current].get_action :upgrade }
+
+ its(:summary) { should =~ /upgrade.*module/im }
+ its(:description) { should =~ /upgrade.*module/im }
+ its(:returns) { should =~ /hash/i }
+ its(:examples) { should_not be_empty }
+
+ %w{ license copyright summary description returns examples }.each do |doc|
+ context "of the" do
+ its(doc.to_sym) { should_not =~ /(FIXME|REVISIT|TODO)/ }
+ end
+ end
+ end
+end
diff --git a/spec/unit/forge/repository_spec.rb b/spec/unit/forge/repository_spec.rb
index 6d8ce38f1..bbfc0d136 100644
--- a/spec/unit/forge/repository_spec.rb
+++ b/spec/unit/forge/repository_spec.rb
@@ -1,86 +1,56 @@
require 'spec_helper'
require 'net/http'
require 'puppet/forge/repository'
require 'puppet/forge/cache'
describe Puppet::Forge::Repository do
describe 'instances' do
let(:repository) { Puppet::Forge::Repository.new('http://fake.com') }
- describe '#make_http_request' do
- before do
- # Do a mock of the Proxy call so we can do proper expects for
- # Net::HTTP
- Net::HTTP.expects(:Proxy).returns(Net::HTTP)
- Net::HTTP.expects(:start)
- end
- context "when not given an :authenticate option" do
- it "should authenticate" do
- repository.expects(:authenticate).never
- repository.make_http_request(nil)
- end
- end
- context "when given an :authenticate option" do
- it "should authenticate" do
- repository.expects(:authenticate)
- repository.make_http_request(nil, :authenticate => true)
- end
- end
- end
-
- describe '#authenticate' do
- it "should set basic auth on the request" do
- authenticated_request = stub
- authenticated_request.expects(:basic_auth)
- repository.expects(:prompt).twice
- repository.authenticate(authenticated_request)
- end
- end
-
describe '#retrieve' do
before do
@uri = URI.parse('http://some.url.com')
end
it "should access the cache" do
repository.cache.expects(:retrieve).with(@uri)
repository.retrieve(@uri)
end
end
describe 'http_proxy support' do
before :each do
ENV["http_proxy"] = nil
end
after :each do
ENV["http_proxy"] = nil
end
it "should support environment variable for port and host" do
ENV["http_proxy"] = "http://test.com:8011"
repository.http_proxy_host.should == "test.com"
repository.http_proxy_port.should == 8011
end
it "should support puppet configuration for port and host" do
ENV["http_proxy"] = nil
Puppet.settings.stubs(:[]).with(:http_proxy_host).returns('test.com')
Puppet.settings.stubs(:[]).with(:http_proxy_port).returns(7456)
repository.http_proxy_port.should == 7456
repository.http_proxy_host.should == "test.com"
end
it "should use environment variable before puppet settings" do
ENV["http_proxy"] = "http://test1.com:8011"
Puppet.settings.stubs(:[]).with(:http_proxy_host).returns('test2.com')
Puppet.settings.stubs(:[]).with(:http_proxy_port).returns(7456)
repository.http_proxy_host.should == "test1.com"
repository.http_proxy_port.should == 8011
end
end
end
end
diff --git a/spec/unit/forge_spec.rb b/spec/unit/forge_spec.rb
index 905f1bd24..95e47a03e 100644
--- a/spec/unit/forge_spec.rb
+++ b/spec/unit/forge_spec.rb
@@ -1,114 +1,56 @@
require 'spec_helper'
require 'puppet/forge'
require 'net/http'
+require 'puppet/module_tool'
+
+describe Puppet::Forge do
+ include PuppetSpec::Files
+
+ let(:response_body) do
+ <<-EOF
+ [
+ {
+ "author": "puppetlabs",
+ "name": "bacula",
+ "tag_list": ["backup", "bacula"],
+ "releases": [{"version": "0.0.1"}, {"version": "0.0.2"}],
+ "full_name": "puppetlabs/bacula",
+ "version": "0.0.2",
+ "project_url": "http://github.com/puppetlabs/puppetlabs-bacula",
+ "desc": "bacula"
+ }
+ ]
+ EOF
+ end
+ let(:response) { stub(:body => response_body, :code => '200') }
-describe Puppet::Forge::Forge do
before do
Puppet::Forge::Repository.any_instance.stubs(:make_http_request).returns(response)
Puppet::Forge::Repository.any_instance.stubs(:retrieve).returns("/tmp/foo")
end
- let(:forge) { forge = Puppet::Forge::Forge.new('http://forge.puppetlabs.com') }
-
describe "the behavior of the search method" do
context "when there are matches for the search term" do
before do
Puppet::Forge::Repository.any_instance.stubs(:make_http_request).returns(response)
end
- let(:response) { stub(:body => response_body, :code => '200') }
- let(:response_body) do
- <<-EOF
- [
- {
- "author": "puppetlabs",
- "name": "bacula",
- "tag_list": ["backup", "bacula"],
- "releases": [{"version": "0.0.1"}, {"version": "0.0.2"}],
- "full_name": "puppetlabs/bacula",
- "version": "0.0.2",
- "project_url": "http://github.com/puppetlabs/puppetlabs-bacula",
- "desc": "bacula"
- }
- ]
- EOF
- end
-
it "should return a list of matches from the forge" do
- forge.search('bacula').should == PSON.load(response_body)
+ Puppet::Forge.search('bacula').should == PSON.load(response_body)
end
end
context "when the connection to the forge fails" do
- let(:response) { stub(:body => '[]', :code => '404') }
+ let(:response) { stub(:body => '{}', :code => '404') }
- it "should raise an error" do
- lambda { forge.search('bacula') }.should raise_error RuntimeError
+ it "should raise an error for search" do
+ lambda { Puppet::Forge.search('bacula') }.should raise_error RuntimeError
end
- end
- end
-
- describe "the behavior of the get_release_package method" do
- let(:response) do
- response = mock()
- response.stubs(:body).returns('{"file": "/system/releases/p/puppetlabs/puppetlabs-apache-0.0.3.tar.gz", "version": "0.0.3"}')
- response
- end
-
- context "when source is not filesystem or repository" do
- it "should raise an error" do
- params = { :source => 'foo' }
- lambda { forge.get_release_package(params) }.should
- raise_error(ArgumentError, "Could not determine installation source")
- end
- end
-
- context "when the source is a repository" do
- let(:params) do
- {
- :source => :repository,
- :author => 'fakeauthor',
- :modname => 'fakemodule',
- :version => '0.0.1'
- }
- end
-
- it "should require author" do
- params.delete(:author)
- lambda { forge.get_release_package(params) }.should
- raise_error(ArgumentError, ":author and :modename required")
- end
-
- it "should require modname" do
- params.delete(:modname)
- lambda { forge.get_release_package(params) }.should
- raise_error(ArgumentError, ":author and :modename required")
- end
-
- it "should download the release package" do
- forge.get_release_package(params).should == "/tmp/foo"
- end
- end
-
- context "when the source is a filesystem" do
- it "should require filename" do
- params = { :source => :filesystem }
- lambda { forge.get_release_package(params) }.should
- raise_error(ArgumentError, ":filename required")
+ it "should raise an error for remote_dependency_info" do
+ lambda { Puppet::Forge.remote_dependency_info('puppetlabs', 'bacula', '0.0.1') }.should raise_error RuntimeError
end
end
end
- describe "the behavior of the get_releases method" do
- let(:response) do
- response = mock()
- response.stubs(:body).returns('{"releases": [{"version": "0.0.1"}, {"version": "0.0.2"}, {"version": "0.0.3"}]}')
- response
- end
-
- it "should return a list of module releases" do
- forge.get_releases('fakeauthor', 'fakemodule').should == ["0.0.1", "0.0.2", "0.0.3"]
- end
- end
end
diff --git a/spec/unit/module_tool/application_spec.rb b/spec/unit/module_tool/application_spec.rb
index b86ec5c39..a2ef184f7 100644
--- a/spec/unit/module_tool/application_spec.rb
+++ b/spec/unit/module_tool/application_spec.rb
@@ -1,29 +1,27 @@
require 'spec_helper'
require 'puppet/module_tool'
-describe Puppet::Module::Tool::Applications::Application do
+describe Puppet::Module::Tool::Applications::Application, :fails_on_windows => true do
describe 'app' do
good_versions = %w{ 1.2.4 0.0.1 0.0.0 0.0.2-git-8-g3d316d1 0.0.3-b1 10.100.10000
0.1.2-rc1 0.1.2-dev-1 0.1.2-svn12345 0.1.2-3 }
bad_versions = %w{ 0.1 0 0.1.2.3 dev 0.1.2beta }
before do
@app = Class.new(described_class).new
end
good_versions.each do |ver|
it "should accept version string #{ver}" do
- @app.instance_eval("@filename=%q{puppetlabs-ntp-#{ver}}")
- @app.parse_filename!
+ @app.parse_filename("puppetlabs-ntp-#{ver}")
end
end
bad_versions.each do |ver|
it "should not accept version string #{ver}" do
- @app.instance_eval("@filename=%q{puppetlabs-ntp-#{ver}}")
- lambda { @app.parse_filename! }.should raise_error
+ lambda { @app.parse_filename("puppetlabs-ntp-#{ver}") }.should raise_error
end
end
end
end
diff --git a/spec/unit/module_tool/applications/application_spec.rb b/spec/unit/module_tool/applications/application_spec.rb
new file mode 100644
index 000000000..101079f61
--- /dev/null
+++ b/spec/unit/module_tool/applications/application_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+require 'puppet/module_tool/applications'
+
+describe Puppet::Module::Tool::Applications do
+ module Puppet::Module::Tool
+ module Applications
+ class Fake < Application
+ end
+ end
+ end
+
+ it "should raise an error on microsoft windows" do
+ Puppet.features.stubs(:microsoft_windows?).returns true
+ expect { Puppet::Module::Tool::Applications::Fake.new }.to raise_error(
+ Puppet::Error,
+ "`puppet module` actions are currently not supported on Microsoft Windows"
+ )
+ end
+end
diff --git a/spec/unit/module_tool/applications/installer_spec.rb b/spec/unit/module_tool/applications/installer_spec.rb
new file mode 100644
index 000000000..1ad80ee60
--- /dev/null
+++ b/spec/unit/module_tool/applications/installer_spec.rb
@@ -0,0 +1,205 @@
+require 'spec_helper'
+require 'puppet/module_tool/applications'
+require 'puppet_spec/modules'
+require 'semver'
+
+describe Puppet::Module::Tool::Applications::Installer, :fails_on_windows => true do
+ include PuppetSpec::Files
+
+ before do
+ FileUtils.mkdir_p(modpath1)
+ fake_env.modulepath = [modpath1]
+ FileUtils.touch(stdlib_pkg)
+ Puppet.settings[:modulepath] = modpath1
+ Puppet::Forge.stubs(:remote_dependency_info).returns(remote_dependency_info)
+ Puppet::Forge.stubs(:repository).returns(repository)
+ end
+
+ let(:unpacker) { stub(:run) }
+ let(:installer_class) { Puppet::Module::Tool::Applications::Installer }
+ let(:modpath1) { File.join(tmpdir("installer"), "modpath1") }
+ let(:stdlib_pkg) { File.join(modpath1, "pmtacceptance-stdlib-0.0.1.tar.gz") }
+ let(:fake_env) { Puppet::Node::Environment.new('fake_env') }
+ let(:options) { Hash[:target_dir => modpath1] }
+
+ let(:repository) do
+ repository = mock()
+ repository.stubs(:uri => 'forge-dev.puppetlabs.com')
+
+ releases = remote_dependency_info.each_key do |mod|
+ remote_dependency_info[mod].each do |release|
+ repository.stubs(:retrieve).with(release['file'])\
+ .returns("/fake_cache#{release['file']}")
+ end
+ end
+
+ repository
+ end
+
+ let(:remote_dependency_info) do
+ {
+ "pmtacceptance/stdlib" => [
+ { "dependencies" => [],
+ "version" => "0.0.1",
+ "file" => "/pmtacceptance-stdlib-0.0.1.tar.gz" },
+ { "dependencies" => [],
+ "version" => "0.0.2",
+ "file" => "/pmtacceptance-stdlib-0.0.2.tar.gz" },
+ { "dependencies" => [],
+ "version" => "1.0.0",
+ "file" => "/pmtacceptance-stdlib-1.0.0.tar.gz" }
+ ],
+ "pmtacceptance/java" => [
+ { "dependencies" => [["pmtacceptance/stdlib", ">= 0.0.1"]],
+ "version" => "1.7.0",
+ "file" => "/pmtacceptance-java-1.7.0.tar.gz" },
+ { "dependencies" => [["pmtacceptance/stdlib", "1.0.0"]],
+ "version" => "1.7.1",
+ "file" => "/pmtacceptance-java-1.7.1.tar.gz" }
+ ],
+ "pmtacceptance/apollo" => [
+ { "dependencies" => [
+ ["pmtacceptance/java", "1.7.1"],
+ ["pmtacceptance/stdlib", "0.0.1"]
+ ],
+ "version" => "0.0.1",
+ "file" => "/pmtacceptance-apollo-0.0.1.tar.gz" },
+ { "dependencies" => [
+ ["pmtacceptance/java", ">= 1.7.0"],
+ ["pmtacceptance/stdlib", ">= 1.0.0"]
+ ],
+ "version" => "0.0.2",
+ "file" => "/pmtacceptance-apollo-0.0.2.tar.gz" }
+ ]
+ }
+ end
+
+ describe "the behavior of .is_module_package?" do
+ it "should return true when file is a module package" do
+ installer = installer_class.new("foo", options)
+ installer.send(:is_module_package?, stdlib_pkg).should be_true
+ end
+
+ it "should return false when file is not a module package" do
+ installer = installer_class.new("foo", options)
+ installer.send(:is_module_package?, "pmtacceptance-apollo-0.0.2.tar").should be_false
+ end
+ end
+
+ context "when the source is a repository" do
+ it "should require a valid name" do
+ lambda { installer_class.run('puppet', params) }.should
+ raise_error(ArgumentError, "Could not install module with invalid name: puppet")
+ end
+
+ it "should install the requested module" do
+ Puppet::Module::Tool::Applications::Unpacker.expects(:new)\
+ .with('/fake_cache/pmtacceptance-stdlib-1.0.0.tar.gz', options)\
+ .returns(unpacker)
+ results = installer_class.run('pmtacceptance-stdlib', options)
+ results[:installed_modules].length == 1
+ results[:installed_modules][0][:module].should == "pmtacceptance-stdlib"
+ results[:installed_modules][0][:version][:vstring].should == "1.0.0"
+ end
+
+ context "when the requested module has dependencies" do
+ it "should install dependencies" do
+ Puppet::Module::Tool::Applications::Unpacker.expects(:new)\
+ .with('/fake_cache/pmtacceptance-stdlib-1.0.0.tar.gz', options)\
+ .returns(unpacker)
+ Puppet::Module::Tool::Applications::Unpacker.expects(:new)\
+ .with('/fake_cache/pmtacceptance-apollo-0.0.2.tar.gz', options)\
+ .returns(unpacker)
+ Puppet::Module::Tool::Applications::Unpacker.expects(:new)\
+ .with('/fake_cache/pmtacceptance-java-1.7.1.tar.gz', options)\
+ .returns(unpacker)
+
+ results = installer_class.run('pmtacceptance-apollo', options)
+ installed_dependencies = results[:installed_modules][0][:dependencies]
+
+ dependencies = installed_dependencies.inject({}) do |result, dep|
+ result[dep[:module]] = dep[:version][:vstring]
+ result
+ end
+
+ dependencies.length.should == 2
+ dependencies['pmtacceptance-java'].should == '1.7.1'
+ dependencies['pmtacceptance-stdlib'].should == '1.0.0'
+ end
+
+ it "should install requested module if the '--force' flag is used" do
+ options = { :force => true, :target_dir => modpath1 }
+ Puppet::Module::Tool::Applications::Unpacker.expects(:new)\
+ .with('/fake_cache/pmtacceptance-apollo-0.0.2.tar.gz', options)\
+ .returns(unpacker)
+ results = installer_class.run('pmtacceptance-apollo', options)
+ results[:installed_modules][0][:module].should == "pmtacceptance-apollo"
+ end
+
+ it "should not install dependencies if the '--force' flag is used" do
+ options = { :force => true, :target_dir => modpath1 }
+ Puppet::Module::Tool::Applications::Unpacker.expects(:new)\
+ .with('/fake_cache/pmtacceptance-apollo-0.0.2.tar.gz', options)\
+ .returns(unpacker)
+ results = installer_class.run('pmtacceptance-apollo', options)
+ dependencies = results[:installed_modules][0][:dependencies]
+ dependencies.should == []
+ end
+
+ it "should not install dependencies if the '--ignore-dependencies' flag is used" do
+ options = { :ignore_dependencies => true, :target_dir => modpath1 }
+ Puppet::Module::Tool::Applications::Unpacker.expects(:new)\
+ .with('/fake_cache/pmtacceptance-apollo-0.0.2.tar.gz', options)\
+ .returns(unpacker)
+ results = installer_class.run('pmtacceptance-apollo', options)
+ dependencies = results[:installed_modules][0][:dependencies]
+ dependencies.should == []
+ end
+
+ it "should set an error if dependencies can't be resolved" do
+ options = { :version => '0.0.1', :target_dir => modpath1 }
+ oneline = "'pmtacceptance-apollo' (v0.0.1) requested; Invalid dependency cycle"
+ multiline = <<-MSG.strip
+Could not install module 'pmtacceptance-apollo' (v0.0.1)
+ No version of 'pmtacceptance-stdlib' will satisfy dependencies
+ You specified 'pmtacceptance-apollo' (v0.0.1),
+ which depends on 'pmtacceptance-java' (v1.7.1),
+ which depends on 'pmtacceptance-stdlib' (v1.0.0)
+ Use `puppet module install --force` to install this module anyway
+MSG
+
+ results = installer_class.run('pmtacceptance-apollo', options)
+ results[:result].should == :failure
+ results[:error][:oneline].should == oneline
+ results[:error][:multiline].should == multiline
+ end
+ end
+
+ context "when there are modules installed" do
+ it "should use local version when already exists and satisfies constraints"
+ it "should reinstall the local version if force is used"
+ it "should upgrade local version when necessary to satisfy constraints"
+ it "should error when a local version can't be upgraded to satisfy constraints"
+ end
+
+ context "when a local module needs upgrading to satisfy constraints but has changes" do
+ it "should error"
+ it "should warn and continue if force is used"
+ end
+
+ it "should error when a local version of a dependency has no version metadata"
+ it "should error when a local version of a dependency has a non-semver version"
+ it "should error when a local version of a dependency has a different forge name"
+ it "should error when a local version of a dependency has no metadata"
+ end
+
+ context "when the source is a filesystem" do
+ before do
+ @sourcedir = tmpdir('sourcedir')
+ end
+
+ it "should error if it can't parse the name"
+
+ it "should try to get_release_package_from_filesystem if it has a valid name"
+ end
+end
diff --git a/spec/unit/module_tool/applications/uninstaller_spec.rb b/spec/unit/module_tool/applications/uninstaller_spec.rb
new file mode 100644
index 000000000..e8a4b3f46
--- /dev/null
+++ b/spec/unit/module_tool/applications/uninstaller_spec.rb
@@ -0,0 +1,206 @@
+require 'spec_helper'
+require 'puppet/module_tool'
+require 'tmpdir'
+require 'puppet_spec/modules'
+
+describe Puppet::Module::Tool::Applications::Uninstaller, :fails_on_windows => true do
+ include PuppetSpec::Files
+
+ def mkmod(name, path, metadata=nil)
+ modpath = File.join(path, name)
+ FileUtils.mkdir_p(modpath)
+
+ if metadata
+ File.open(File.join(modpath, 'metadata.json'), 'w') do |f|
+ f.write(metadata.to_pson)
+ end
+ end
+
+ modpath
+ end
+
+ describe "the behavior of the instances" do
+
+ before do
+ @uninstaller = Puppet::Module::Tool::Applications::Uninstaller
+ FileUtils.mkdir_p(modpath1)
+ FileUtils.mkdir_p(modpath2)
+ fake_env.modulepath = [modpath1, modpath2]
+ end
+
+ let(:modpath1) { File.join(tmpdir("uninstaller"), "modpath1") }
+ let(:modpath2) { File.join(tmpdir("uninstaller"), "modpath2") }
+ let(:fake_env) { Puppet::Node::Environment.new('fake_env') }
+ let(:options) { {:environment => "fake_env"} }
+
+ let(:foo_metadata) do
+ {
+ :author => "puppetlabs",
+ :name => "puppetlabs/foo",
+ :version => "1.0.0",
+ :source => "http://dummyurl/foo",
+ :license => "Apache2",
+ :dependencies => [],
+ }
+ end
+
+ let(:bar_metadata) do
+ {
+ :author => "puppetlabs",
+ :name => "puppetlabs/bar",
+ :version => "1.0.0",
+ :source => "http://dummyurl/bar",
+ :license => "Apache2",
+ :dependencies => [],
+ }
+ end
+
+ context "when the module is not installed" do
+ it "should fail" do
+ @uninstaller.new('fakemod_not_installed', options).run[:result].should == :failure
+ end
+ end
+
+ context "when the module is installed" do
+
+ it "should uninstall the module" do
+ PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
+
+ results = @uninstaller.new("puppetlabs-foo", options).run
+ results[:affected_modules].first.forge_name.should == "puppetlabs/foo"
+ end
+
+ it "should only uninstall the requested module" do
+ PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
+ PuppetSpec::Modules.create('bar', modpath1, :metadata => bar_metadata)
+
+ results = @uninstaller.new("puppetlabs-foo", options).run
+ results[:affected_modules].length == 1
+ results[:affected_modules].first.forge_name.should == "puppetlabs/foo"
+ end
+
+ it "should uninstall fail if a module exists twice in the modpath" do
+ PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
+ PuppetSpec::Modules.create('foo', modpath2, :metadata => foo_metadata)
+
+ @uninstaller.new('puppetlabs-foo', options).run[:result].should == :failure
+ end
+
+ context "when options[:version] is specified" do
+
+ it "should uninstall the module if the version matches" do
+ PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
+
+ options[:version] = "1.0.0"
+
+ results = @uninstaller.new("puppetlabs-foo", options).run
+ results[:affected_modules].length.should == 1
+ results[:affected_modules].first.forge_name.should == "puppetlabs/foo"
+ results[:affected_modules].first.version.should == "1.0.0"
+ end
+
+ it "should not uninstall the module if the version does not match" do
+ PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
+
+ options[:version] = "2.0.0"
+
+ @uninstaller.new("puppetlabs-foo", options).run[:result].should == :failure
+ end
+ end
+
+ context "when the module metadata is missing" do
+
+ it "should not uninstall the module" do
+ PuppetSpec::Modules.create('foo', modpath1)
+
+ @uninstaller.new("puppetlabs-foo", options).run[:result].should == :failure
+ end
+ end
+
+ context "when the module has local changes" do
+
+ it "should not uninstall the module" do
+ PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
+ Puppet::Module.any_instance.stubs(:has_local_changes?).returns(true)
+
+ @uninstaller.new("puppetlabs-foo", options).run[:result].should == :failure
+ end
+
+ end
+
+ context "when the module does not have local changes" do
+
+ it "should uninstall the module" do
+ PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
+
+ results = @uninstaller.new("puppetlabs-foo", options).run
+ results[:affected_modules].length.should == 1
+ results[:affected_modules].first.forge_name.should == "puppetlabs/foo"
+ end
+ end
+
+ context "when uninstalling the module will cause broken dependencies" do
+ it "should not uninstall the module" do
+ Puppet.settings[:modulepath] = modpath1
+ PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
+
+ PuppetSpec::Modules.create(
+ 'needy',
+ modpath1,
+ :metadata => {
+ :author => 'beggar',
+ :dependencies => [{
+ "version_requirement" => ">= 1.0.0",
+ "name" => "puppetlabs/foo"
+ }]
+ }
+ )
+
+ @uninstaller.new("puppetlabs-foo", options).run[:result].should == :failure
+ end
+ end
+
+ context "when using the --force flag" do
+
+ let(:fakemod) do
+ stub(
+ :forge_name => 'puppetlabs/fakemod',
+ :version => '0.0.1',
+ :has_local_changes? => true
+ )
+ end
+
+ it "should ignore local changes" do
+ foo = mkmod("foo", modpath1, foo_metadata)
+ options[:force] = true
+
+ results = @uninstaller.new("puppetlabs-foo", options).run
+ results[:affected_modules].length.should == 1
+ results[:affected_modules].first.forge_name.should == "puppetlabs/foo"
+ end
+
+ it "should ignore broken dependencies" do
+ Puppet.settings[:modulepath] = modpath1
+ PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
+
+ PuppetSpec::Modules.create(
+ 'needy',
+ modpath1,
+ :metadata => {
+ :author => 'beggar',
+ :dependencies => [{
+ "version_requirement" => ">= 1.0.0",
+ "name" => "puppetlabs/foo"
+ }]
+ }
+ )
+ options[:force] = true
+
+ results = @uninstaller.new("puppetlabs-foo", options).run
+ results[:affected_modules].length.should == 1
+ results[:affected_modules].first.forge_name.should == "puppetlabs/foo"
+ 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
new file mode 100644
index 000000000..542e00b7a
--- /dev/null
+++ b/spec/unit/module_tool/applications/upgrader_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+require 'puppet/module_tool/applications'
+require 'puppet_spec/modules'
+require 'semver'
+
+describe Puppet::Module::Tool::Applications::Upgrader, :fails_on_windows => true do
+ include PuppetSpec::Files
+
+ before do
+ end
+
+ it "should update the requested module"
+ it "should not update dependencies"
+ it "should fail when updating a dependency to an unsupported version"
+ it "should fail when updating a module that is not installed"
+ it "should warn when the latest version is already installed"
+ it "should warn when the best version is already installed"
+
+ context "when using the '--version' option" do
+ it "should update an installed module to the requested version"
+ end
+
+ context "when using the '--force' flag" do
+ it "should ignore missing dependencies"
+ it "should ignore version constraints"
+ it "should not update a module that is not installed"
+ end
+
+ context "when using the '--env' option" do
+ it "should use the correct environment"
+ end
+
+ context "when there are missing dependencies" do
+ it "should fail to upgrade the original module"
+ it "should raise an error"
+ end
+end
diff --git a/spec/unit/module_tool/uninstaller_spec.rb b/spec/unit/module_tool/uninstaller_spec.rb
deleted file mode 100644
index 7a5a93aff..000000000
--- a/spec/unit/module_tool/uninstaller_spec.rb
+++ /dev/null
@@ -1,124 +0,0 @@
-require 'spec_helper'
-require 'puppet/module_tool'
-require 'tmpdir'
-
-describe Puppet::Module::Tool::Applications::Uninstaller do
- include PuppetSpec::Files
-
- def mkmod(name, path, metadata=nil)
- modpath = File.join(path, name)
- FileUtils.mkdir_p(modpath)
-
- # For some tests we need the metadata to be present, mainly
- # when testing against specific versions of a module.
- if metadata
- File.open(File.join(modpath, 'metadata.json'), 'w') do |f|
- f.write(metadata.to_pson)
- end
- end
-
- modpath
- end
-
- describe "the behavior of the instances" do
-
- before do
- @uninstaller = Puppet::Module::Tool::Applications::Uninstaller
- FileUtils.mkdir_p(modpath1)
- FileUtils.mkdir_p(modpath2)
- fake_env.modulepath = [modpath1, modpath2]
- end
-
- let(:modpath1) { File.join(tmpdir("uninstaller"), "modpath1") }
- let(:modpath2) { File.join(tmpdir("uninstaller"), "modpath2") }
- let(:fake_env) { Puppet::Node::Environment.new('fake_env') }
- let(:options) { {:environment => "fake_env"} }
-
- context "when the module is not installed" do
- it "should return an empty list" do
- results = @uninstaller.new('fakemod_not_installed', options).run
- results[:removed_mods].should == []
- end
- end
-
- context "when the module is installed" do
- it "should uninstall the module" do
- foo = mkmod("foo", modpath1)
-
- results = @uninstaller.new("foo", options).run
- results[:removed_mods].should == [
- Puppet::Module.new('foo', :environment => fake_env, :path => foo)
- ]
- end
-
- it "should only uninstall the requested module" do
- foo = mkmod("foo", modpath1)
-
- results = @uninstaller.new("foo", options).run
- results[:removed_mods].should == [
- Puppet::Module.new("foo", :environment => fake_env, :path => foo)
- ]
- end
-
- it "should uninstall the module from every path in the modpath" do
- foo1 = mkmod('foo', modpath1)
- foo2 = mkmod('foo', modpath2)
-
- results = @uninstaller.new('foo', options).run
- results[:removed_mods].length.should == 2
- results[:removed_mods].should include(
- Puppet::Module.new('foo', :environment => fake_env, :path => foo1),
- Puppet::Module.new('foo', :environment => fake_env, :path => foo2)
- )
- end
-
- context "when options[:version] is specified" do
- let(:metadata) do
- {
- "author" => "",
- "name" => "foo",
- "version" => "1.0.0",
- "source" => "http://dummyurl",
- "license" => "Apache2",
- "dependencies" => [],
- }
- end
-
- it "should uninstall the module if the version matches" do
- foo = mkmod('foo', modpath1, metadata)
-
- options[:version] = "1.0.0"
-
- results = @uninstaller.new("foo", options).run
- results[:removed_mods].length.should == 1
- results[:removed_mods].first.name.should == "foo"
- results[:removed_mods].first.version.should == "1.0.0"
- end
-
- it "should not uninstall the module if the version does not match" do
- foo = mkmod("foo", modpath1, metadata)
-
- options[:version] = "2.0.0"
-
- results = @uninstaller.new("foo", options).run
- results[:removed_mods].should == []
- end
-
- context "when the module metadata is missing" do
- it "should not uninstall the module" do
- foo = mkmod("foo", modpath1)
-
- options[:version] = "2.0.0"
-
- results = @uninstaller.new("foo", options).run
- results[:removed_mods].should == []
- end
- end
- end
-
- # This test is pending work in #11803 to which will add
- # dependency resolution.
- it "should check for broken dependencies"
- end
- end
-end
diff --git a/spec/unit/module_tool_spec.rb b/spec/unit/module_tool_spec.rb
index 86d421e69..5ddb01fe1 100644
--- a/spec/unit/module_tool_spec.rb
+++ b/spec/unit/module_tool_spec.rb
@@ -1,5 +1,113 @@
+# encoding: UTF-8
+
require 'spec_helper'
require 'puppet/module_tool'
-describe Puppet::Module::Tool do
+describe Puppet::Module::Tool, :fails_on_windows => true do
+ describe '.build_tree' do
+ it 'should return an empty tree when given an empty list' do
+ subject.build_tree([]).should == ''
+ end
+
+ it 'should return a shallow when given a list without dependencies' do
+ list = [ { :text => 'first' }, { :text => 'second' }, { :text => 'third' } ]
+ subject.build_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.build_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.build_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.build_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.build_tree(list).should == <<-TREE
+├─┬ first
+│ ├─┬ second
+│ │ └── third
+│ └── fourth
+└── fifth
+TREE
+ end
+ end
end
diff --git a/spec/unit/util/terminal_spec.rb b/spec/unit/util/terminal_spec.rb
new file mode 100644
index 000000000..d70bb94a9
--- /dev/null
+++ b/spec/unit/util/terminal_spec.rb
@@ -0,0 +1,42 @@
+#!/usr/bin/env rspec
+require 'spec_helper'
+require 'puppet/util/terminal'
+
+describe Puppet::Util::Terminal do
+ describe '.width' do
+ before { Puppet.features.stubs(:posix?).returns(true) }
+
+ it 'should invoke `stty` and return the width' do
+ height, width = 100, 200
+ subject.expects(:`).with('stty size 2>/dev/null').returns("#{height} #{width}\n")
+ subject.width.should == width
+ end
+
+ it 'should use `tput` if `stty` is unavailable' do
+ width = 200
+ subject.expects(:`).with('stty size 2>/dev/null').returns("\n")
+ subject.expects(:`).with('tput cols 2>/dev/null').returns("#{width}\n")
+ subject.width.should == width
+ end
+
+ it 'should default to 80 columns if `tput` and `stty` are unavailable' do
+ width = 80
+ subject.expects(:`).with('stty size 2>/dev/null').returns("\n")
+ subject.expects(:`).with('tput cols 2>/dev/null').returns("\n")
+ subject.width.should == width
+ end
+
+ it 'should default to 80 columns if `tput` or `stty` raise exceptions' do
+ width = 80
+ subject.expects(:`).with('stty size 2>/dev/null').raises()
+ subject.stubs(:`).with('tput cols 2>/dev/null').returns("#{width + 1000}\n")
+ subject.width.should == width
+ end
+
+ it 'should default to 80 columns if not in a POSIX environment' do
+ width = 80
+ Puppet.features.stubs(:posix?).returns(false)
+ subject.width.should == width
+ end
+ end
+end