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