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.rb b/lib/puppet/module.rb index b68cd87e8..db27abbc4 100644 --- a/lib/puppet/module.rb +++ b/lib/puppet/module.rb @@ -1,263 +1,314 @@ require 'puppet/util/logging' require 'semver' require 'puppet/module_tool/applications' # Support for modules class Puppet::Module class Error < Puppet::Error; end class MissingModule < Error; end class IncompatibleModule < Error; end class UnsupportedPlatform < Error; end class IncompatiblePlatform < Error; end class MissingMetadata < Error; end class InvalidName < Error; end include Puppet::Util::Logging TEMPLATES = "templates" FILES = "files" MANIFESTS = "manifests" PLUGINS = "plugins" FILETYPES = [MANIFESTS, FILES, TEMPLATES, PLUGINS] # Find and return the +module+ that +path+ belongs to. If +path+ is # absolute, or if there is no module whose name is the first component # of +path+, return +nil+ def self.find(modname, environment = nil) return nil unless modname Puppet::Node::Environment.new(environment).module(modname) end attr_reader :name, :environment attr_writer :environment attr_accessor :dependencies, :forge_name attr_accessor :source, :author, :version, :license, :puppetversion, :summary, :description, :project_page def has_metadata? return false unless metadata_file return false unless FileTest.exist?(metadata_file) metadata = PSON.parse File.read(metadata_file) return metadata.is_a?(Hash) && !metadata.keys.empty? end def initialize(name, options = {}) @name = name @path = options[:path] assert_validity if options[:environment].is_a?(Puppet::Node::Environment) @environment = options[:environment] else @environment = Puppet::Node::Environment.new(options[:environment]) end load_metadata if has_metadata? validate_puppet_version end FILETYPES.each do |type| # A boolean method to let external callers determine if # we have files of a given type. define_method(type +'?') do return false unless path return false unless FileTest.exist?(subpath(type)) return true end # A method for returning a given file of a given type. # e.g., file = mod.manifest("my/manifest.pp") # # If the file name is nil, then the base directory for the # file type is passed; this is used for fileserving. define_method(type.to_s.sub(/s$/, '')) do |file| return nil unless path # If 'file' is nil then they're asking for the base path. # This is used for things like fileserving. if file full_path = File.join(subpath(type), file) else full_path = subpath(type) end return nil unless FileTest.exist?(full_path) return full_path end end def exist? ! path.nil? end # Find the first 'files' directory. This is used by the XMLRPC fileserver. def file_directory subpath("files") end def license_file return @license_file if defined?(@license_file) return @license_file = nil unless path @license_file = File.join(path, "License") end def load_metadata data = PSON.parse File.read(metadata_file) @forge_name = data['name'].gsub('-', '/') if data['name'] [:source, :author, :version, :license, :puppetversion, :dependencies].each do |attr| unless value = data[attr.to_s] unless attr == :puppetversion raise MissingMetadata, "No #{attr} module metadata provided for #{self.name}" end end + + # NOTICE: The fallback to `versionRequirement` is something we'd like to + # not have to support, but we have a reasonable number of releases that + # don't use `version_requirement`. When we can deprecate this, we should. + if attr == :dependencies + value.tap do |dependencies| + dependencies.each do |dep| + dep['version_requirement'] ||= dep['versionRequirement'] || '>= 0.0.0' + end + end + end + send(attr.to_s + "=", value) end end # Return the list of manifests matching the given glob pattern, # defaulting to 'init.{pp,rb}' for empty modules. def match_manifests(rest) pat = File.join(path, MANIFESTS, rest || 'init') [manifest("init.pp"),manifest("init.rb")].compact + Dir. glob(pat + (File.extname(pat).empty? ? '.{pp,rb}' : '')). reject { |f| FileTest.directory?(f) } end def metadata_file return @metadata_file if defined?(@metadata_file) return @metadata_file = nil unless path @metadata_file = File.join(path, "metadata.json") end # Find this module in the modulepath. def path @path ||= environment.modulepath.collect { |path| File.join(path, name) }.find { |d| FileTest.directory?(d) } end + def modulepath + File.dirname(path) if path + end + # Find all plugin directories. This is used by the Plugins fileserving mount. def plugin_directory subpath("plugins") end def supports(name, version = nil) @supports ||= [] @supports << [name, version] end def to_s result = "Module #{name}" result += "(#{path})" if path result end def dependencies_as_modules dependent_modules = [] dependencies and dependencies.each do |dep| author, dep_name = dep["name"].split('/') found_module = environment.module(dep_name) dependent_modules << found_module if found_module end dependent_modules end def required_by environment.module_requirements[self.forge_name] || {} end def has_local_changes? changes = Puppet::Module::Tool::Applications::Checksummer.run(path) !changes.empty? end - def unmet_dependencies - return [] unless dependencies + def local_changes + Puppet::Module::Tool::Applications::Checksummer.run(path) + end + # Identify and mark unmet dependencies. A dependency will be marked unmet + # for the following reasons: + # + # * not installed and is thus considered missing + # * installed and does not meet the version requirements for this module + # * installed and doesn't use semantic versioning + # + # Returns a list of hashes representing the details of an unmet dependency. + # + # Example: + # + # [ + # { + # :reason => :missing, + # :name => 'puppetlabs-mysql', + # :version_constraint => 'v0.0.1', + # :mod_details => { + # :installed_version => '0.0.1' + # } + # :parent => { + # :name => 'puppetlabs-bacula', + # :version => 'v1.0.0' + # } + # } + # ] + # + def unmet_dependencies unmet_dependencies = [] + return unmet_dependencies unless dependencies dependencies.each do |dependency| forge_name = dependency['name'] - author, dep_name = forge_name.split('/') - version_string = dependency['version_requirement'] + version_string = dependency['version_requirement'] || '>= 0.0.0' - equality, dep_version = version_string ? version_string.split("\s") : [nil, nil] - - unless dep_mod = environment.module(dep_name) - msg = "Missing dependency `#{dep_name}`:\n" - msg += " `#{self.name}` (#{self.version}) requires `#{forge_name}` (#{version_string})\n" - unmet_dependencies << { :name => forge_name, :error => msg } - next + dep_mod = begin + environment.module_by_forge_name(forge_name) + rescue => e + nil end - if dep_version && !dep_mod.version - msg = "Unversioned dependency `#{dep_mod.name}`:\n" - msg += " `#{self.name}` (#{self.version}) requires `#{forge_name}` (#{version_string})\n" - unmet_dependencies << { :name => forge_name, :error => msg } + error_details = { + :name => forge_name, + :version_constraint => version_string.gsub(/^(?=\d)/, "v"), + :parent => { + :name => self.forge_name, + :version => self.version.gsub(/^(?=\d)/, "v") + }, + :mod_details => { + :installed_version => dep_mod.nil? ? nil : dep_mod.version + } + } + + unless dep_mod + error_details[:reason] = :missing + unmet_dependencies << error_details next end - if dep_version + if version_string begin - required_version_semver = SemVer.new(dep_version) + required_version_semver_range = SemVer[version_string] actual_version_semver = SemVer.new(dep_mod.version) rescue ArgumentError - msg = "Non semantic version dependency `#{dep_mod.name}` (#{dep_mod.version}):\n" - msg += " `#{self.name}` (#{self.version}) requires `#{forge_name}` (#{version_string})\n" - unmet_dependencies << { :name => forge_name, :error => msg } + error_details[:reason] = :non_semantic_version + unmet_dependencies << error_details next end - if !actual_version_semver.send(equality, required_version_semver) - msg = "Version dependency mismatch `#{dep_mod.name}` (#{dep_mod.version}):\n" - msg += " `#{self.name}` (#{self.version}) requires `#{forge_name}` (#{version_string})\n" - unmet_dependencies << { :name => forge_name, :error => msg } + unless required_version_semver_range.include? actual_version_semver + error_details[:reason] = :version_mismatch + unmet_dependencies << error_details next end end end + unmet_dependencies end def validate_puppet_version return unless puppetversion and puppetversion != Puppet.version raise IncompatibleModule, "Module #{self.name} is only compatible with Puppet version #{puppetversion}, not #{Puppet.version}" end private def subpath(type) return File.join(path, type) unless type.to_s == "plugins" backward_compatible_plugins_dir end def backward_compatible_plugins_dir if dir = File.join(path, "plugins") and FileTest.exist?(dir) Puppet.warning "using the deprecated 'plugins' directory for ruby extensions; please move to 'lib'" return dir else return File.join(path, "lib") end end def assert_validity raise InvalidName, "Invalid module name #{name}; module names must be alphanumeric (plus '-'), not '#{name}'" unless name =~ /^[-\w]+$/ end def ==(other) self.name == other.name && - self.version == other.version && - self.path == other.path && - self.environment == other.environment + self.version == other.version && + self.path == other.path && + self.environment == other.environment 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/node/environment.rb b/lib/puppet/node/environment.rb index 717b9f28a..b16d15137 100644 --- a/lib/puppet/node/environment.rb +++ b/lib/puppet/node/environment.rb @@ -1,215 +1,217 @@ require 'puppet/util' require 'puppet/util/cacher' require 'monitor' # Just define it, so this class has fewer load dependencies. class Puppet::Node end # Model the environment that a node can operate in. This class just # provides a simple wrapper for the functionality around environments. class Puppet::Node::Environment module Helper def environment Puppet::Node::Environment.new(@environment) end def environment=(env) if env.is_a?(String) or env.is_a?(Symbol) @environment = env else @environment = env.name end end end include Puppet::Util::Cacher @seen = {} # Return an existing environment instance, or create a new one. def self.new(name = nil) return name if name.is_a?(self) name ||= Puppet.settings.value(:environment) raise ArgumentError, "Environment name must be specified" unless name symbol = name.to_sym return @seen[symbol] if @seen[symbol] obj = self.allocate obj.send :initialize, symbol @seen[symbol] = obj end def self.current Thread.current[:environment] || root end def self.current=(env) Thread.current[:environment] = new(env) end def self.root @root end def self.clear @seen.clear end attr_reader :name # Return an environment-specific setting. def [](param) Puppet.settings.value(param, self.name) end def initialize(name) @name = name extend MonitorMixin end def known_resource_types # This makes use of short circuit evaluation to get the right thread-safe # per environment semantics with an efficient most common cases; we almost # always just return our thread's known-resource types. Only at the start # of a compilation (after our thread var has been set to nil) or when the # environment has changed do we delve deeper. Thread.current[:known_resource_types] = nil if (krt = Thread.current[:known_resource_types]) && krt.environment != self Thread.current[:known_resource_types] ||= synchronize { if @known_resource_types.nil? or @known_resource_types.require_reparse? @known_resource_types = Puppet::Resource::TypeCollection.new(self) @known_resource_types.import_ast(perform_initial_import, '') end @known_resource_types } end def module(name) mod = Puppet::Module.new(name, :environment => self) return nil unless mod.exist? mod end def module_by_forge_name(forge_name) author, modname = forge_name.split('/') found_mod = self.module(modname) found_mod and found_mod.forge_name == forge_name ? found_mod : nil end # Cache the modulepath, so that we aren't searching through # all known directories all the time. cached_attr(:modulepath, Puppet[:filetimeout]) do dirs = self[:modulepath].split(File::PATH_SEPARATOR) dirs = ENV["PUPPETLIB"].split(File::PATH_SEPARATOR) + dirs if ENV["PUPPETLIB"] validate_dirs(dirs) end # Return all modules from this environment. # Cache the list, because it can be expensive to create. cached_attr(:modules, Puppet[:filetimeout]) do module_names = modulepath.collect { |path| Dir.entries(path) }.flatten.uniq module_names.collect do |path| begin Puppet::Module.new(path, :environment => self) rescue Puppet::Module::Error => e nil end end.compact end # Modules broken out by directory in the modulepath def modules_by_path modules_by_path = {} modulepath.each do |path| Dir.chdir(path) do - module_names = Dir.glob('*').select { |d| FileTest.directory? d } - modules_by_path[path] = module_names.map do |name| + module_names = Dir.glob('*').select do |d| + FileTest.directory?(d) && (File.basename(d) =~ /^[-\w]+$/) + end + modules_by_path[path] = module_names.sort.map do |name| Puppet::Module.new(name, :environment => self, :path => File.join(path, name)) end end end modules_by_path end def module_requirements deps = {} modules.each do |mod| next unless mod.forge_name deps[mod.forge_name] ||= [] mod.dependencies and mod.dependencies.each do |mod_dep| deps[mod_dep['name']] ||= [] dep_details = { 'name' => mod.forge_name, 'version' => mod.version, 'version_requirement' => mod_dep['version_requirement'] } deps[mod_dep['name']] << dep_details end end deps.each do |mod, mod_deps| deps[mod] = mod_deps.sort_by {|d| d['name']} end deps end def to_s name.to_s end def to_sym to_s.to_sym end # The only thing we care about when serializing an environment is its # identity; everything else is ephemeral and should not be stored or # transmitted. def to_zaml(z) self.to_s.to_zaml(z) end def validate_dirs(dirs) dirs.collect do |dir| unless Puppet::Util.absolute_path?(dir) File.expand_path(File.join(Dir.getwd, dir)) else dir end end.find_all do |p| Puppet::Util.absolute_path?(p) && FileTest.directory?(p) end end private def perform_initial_import return empty_parse_result if Puppet.settings[:ignoreimport] parser = Puppet::Parser::Parser.new(self) if code = Puppet.settings.uninterpolated_value(:code, name.to_s) and code != "" parser.string = code else file = Puppet.settings.value(:manifest, name.to_s) parser.file = file end parser.parse rescue => detail known_resource_types.parse_failed = true msg = "Could not parse for environment #{self}: #{detail}" error = Puppet::Error.new(msg) error.set_backtrace(detail.backtrace) raise error end def empty_parse_result # Return an empty toplevel hostclass to use as the result of # perform_initial_import when no file was actually loaded. return Puppet::Parser::AST::Hostclass.new('') end @root = new(:'*root*') end diff --git a/lib/puppet/util/monkey_patches.rb b/lib/puppet/util/monkey_patches.rb index 010e9b1d3..8fd566e24 100644 --- a/lib/puppet/util/monkey_patches.rb +++ b/lib/puppet/util/monkey_patches.rb @@ -1,172 +1,217 @@ unless defined? JRUBY_VERSION Process.maxgroups = 1024 end module RDoc def self.caller(skip=nil) in_gem_wrapper = false Kernel.caller.reject { |call| in_gem_wrapper ||= call =~ /#{Regexp.escape $0}:\d+:in `load'/ } end end require "yaml" require "puppet/util/zaml.rb" class Symbol def to_zaml(z) z.emit("!ruby/sym ") to_s.to_zaml(z) end def <=> (other) self.to_s <=> other.to_s end end [Object, Exception, Integer, Struct, Date, Time, Range, Regexp, Hash, Array, Float, String, FalseClass, TrueClass, Symbol, NilClass, Class].each { |cls| cls.class_eval do def to_yaml(ignored=nil) ZAML.dump(self) end end } def YAML.dump(*args) ZAML.dump(*args) end # # Workaround for bug in MRI 1.8.7, see # http://redmine.ruby-lang.org/issues/show/2708 # for details # if RUBY_VERSION == '1.8.7' class NilClass def closed? true end end end class Object # ActiveSupport 2.3.x mixes in a dangerous method # that can cause rspec to fork bomb # and other strange things like that. def daemonize raise NotImplementedError, "Kernel.daemonize is too dangerous, please don't try to use it." end # The following code allows callers to make assertions that are only # checked when the environment variable PUPPET_ENABLE_ASSERTIONS is # set to a non-empty string. For example: # # assert_that { condition } # assert_that(message) { condition } if ENV["PUPPET_ENABLE_ASSERTIONS"].to_s != '' def assert_that(message = nil) unless yield raise Exception.new("Assertion failure: #{message}") end end else def assert_that(message = nil) end end end # Workaround for yaml_initialize, which isn't supported before Ruby # 1.8.3. if RUBY_VERSION == '1.8.1' || RUBY_VERSION == '1.8.2' YAML.add_ruby_type( /^object/ ) { |tag, val| type, obj_class = YAML.read_type_class( tag, Object ) r = YAML.object_maker( obj_class, val ) if r.respond_to? :yaml_initialize r.instance_eval { instance_variables.each { |name| remove_instance_variable name } } r.yaml_initialize(tag, val) end r } end class Array # Ruby < 1.8.7 doesn't have this method but we use it in tests def combination(num) return [] if num < 0 || num > size return [[]] if num == 0 return map{|e| [e] } if num == 1 tmp = self.dup self[0, size - (num - 1)].inject([]) do |ret, e| tmp.shift ret += tmp.combination(num - 1).map{|a| a.unshift(e) } end end unless method_defined? :combination alias :count :length unless method_defined? :count end class Symbol def to_proc Proc.new { |*args| args.shift.__send__(self, *args) } end unless method_defined? :to_proc end class String def lines(separator = $/) lines = split(separator) block_given? and lines.each {|line| yield line } lines end end class IO def lines(separator = $/) lines = split(separator) block_given? and lines.each {|line| yield line } lines end def self.binread(name, length = nil, offset = 0) File.open(name, 'rb') do |f| f.seek(offset) if offset > 0 f.read(length) end end unless singleton_methods.include?(:binread) def self.binwrite(name, string, offset = 0) File.open(name, 'wb') do |f| f.write(offset > 0 ? string[offset..-1] : string) end end unless singleton_methods.include?(:binwrite) end class Range def intersection(other) raise ArgumentError, 'value must be a Range' unless other.kind_of?(Range) return unless other === self.first || self === other.first start = [self.first, other.first].max if self.exclude_end? && self.last <= other.last start ... self.last elsif other.exclude_end? && self.last >= other.last start ... other.last else start .. [ self.last, other.last ].min end end unless method_defined? :intersection alias_method :&, :intersection unless method_defined? :& end # Ruby 1.8.5 doesn't have tap module Kernel def tap yield(self) self end unless method_defined?(:tap) end + +# The mv method in Ruby 1.8.5 can't mv directories across devices +# File.rename causes "Invalid cross-device link", which is rescued, but in Ruby +# 1.8.5 it tries to recover with a copy and unlink, but the unlink causes the +# error "Is a directory". In newer Rubies remove_entry is used +# The implementation below is what's used in Ruby 1.8.7 and Ruby 1.9 +if RUBY_VERSION == '1.8.5' + require 'fileutils' + + module FileUtils + def mv(src, dest, options = {}) + fu_check_options options, OPT_TABLE['mv'] + fu_output_message "mv#{options[:force] ? ' -f' : ''} #{[src,dest].flatten.join ' '}" if options[:verbose] + return if options[:noop] + fu_each_src_dest(src, dest) do |s, d| + destent = Entry_.new(d, nil, true) + begin + if destent.exist? + if destent.directory? + raise Errno::EEXIST, dest + else + destent.remove_file if rename_cannot_overwrite_file? + end + end + begin + File.rename s, d + rescue Errno::EXDEV + copy_entry s, d, true + if options[:secure] + remove_entry_secure s, options[:force] + else + remove_entry s, options[:force] + end + end + rescue SystemCallError + raise unless options[:force] + end + end + end + module_function :mv + + alias move mv + module_function :move + 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/lib/semver.rb b/lib/semver.rb index 5029d96b4..53c7e697a 100644 --- a/lib/semver.rb +++ b/lib/semver.rb @@ -1,116 +1,121 @@ require 'puppet/util/monkey_patches' # We need to subclass Numeric to force range comparisons not to try to iterate over SemVer # and instead use numeric comparisons (eg >, <, >=, <=) # Ruby 1.8 already did this for all ranges, but Ruby 1.9 changed range include behavior class SemVer < Numeric include Comparable VERSION = /^v?(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z-]*|)$/ SIMPLE_RANGE = /^v?(\d+|[xX])(?:\.(\d+|[xX])(?:\.(\d+|[xX]))?)?$/ def self.valid?(ver) VERSION =~ ver end def self.find_matching(pattern, versions) versions.select { |v| v.matched_by?("#{pattern}") }.sort.last end def self.[](range) + pre = proc { |vstring| vstring =~ /-/ ? vstring : vstring + '-' } range.gsub(/([><=])\s+/, '\1').split(/\b\s+(?!-)/).map do |r| case r when SemVer::VERSION - SemVer.new(r + '-') .. SemVer.new(r) + SemVer.new(pre[r]) .. SemVer.new(r) when SemVer::SIMPLE_RANGE r += ".0" unless SemVer.valid?(r.gsub(/x/i, '0')) SemVer.new(r.gsub(/x/i, '0'))...SemVer.new(r.gsub(/(\d+)\.x/i) { "#{$1.to_i + 1}.0" } + '-') when /\s+-\s+/ a, b = r.split(/\s+-\s+/) - SemVer.new(a + '-') .. SemVer.new(b) + SemVer.new(pre[a]) .. SemVer.new(b) when /^~/ ver = r.sub(/~/, '').split('.').map(&:to_i) start = (ver + [0] * (3 - ver.length)).join('.') ver.pop unless ver.length == 1 ver[-1] = ver.last + 1 finish = (ver + [0] * (3 - ver.length)).join('.') - SemVer.new(start + '-') ... SemVer.new(finish + '-') + SemVer.new(pre[start]) ... SemVer.new(pre[finish]) when /^>=/ ver = r.sub(/^>=/, '') - SemVer.new(ver + '-') .. SemVer::MAX + SemVer.new(pre[ver]) .. SemVer::MAX when /^<=/ ver = r.sub(/^<=/, '') SemVer::MIN .. SemVer.new(ver) when /^>/ - ver = r.sub(/^>/, '').split('.').map(&:to_i) - ver[2] = ver.last + 1 + if r =~ /-/ + ver = [r[1..-1]] + else + ver = r.sub(/^>/, '').split('.').map(&:to_i) + ver[2] = ver.last + 1 + end SemVer.new(ver.join('.') + '-') .. SemVer::MAX when /^(other) other = SemVer.new("#{other}") unless other.is_a? SemVer return self.major <=> other.major unless self.major == other.major return self.minor <=> other.minor unless self.minor == other.minor return self.tiny <=> other.tiny unless self.tiny == other.tiny return 0 if self.special == other.special return 1 if self.special == '' return -1 if other.special == '' return self.special <=> other.special end def matched_by?(pattern) # For the time being, this is restricted to exact version matches and # simple range patterns. In the future, we should implement some or all of # the comparison operators here: # https://github.com/isaacs/node-semver/blob/d474801/semver.js#L340 case pattern when SIMPLE_RANGE pattern = SIMPLE_RANGE.match(pattern).captures pattern[1] = @minor unless pattern[1] && pattern[1] !~ /x/i pattern[2] = @tiny unless pattern[2] && pattern[2] !~ /x/i [@major, @minor, @tiny] == pattern.map { |x| x.to_i } when VERSION self == SemVer.new(pattern) else false end end def inspect @vstring || "v#{@major}.#{@minor}.#{@tiny}#{@special}" end alias :to_s :inspect MIN = SemVer.new('0.0.0-') MIN.instance_variable_set(:@vstring, 'vMIN') MAX = SemVer.new('8.0.0') MAX.instance_variable_set(:@major, (1.0/0)) # => Infinity MAX.instance_variable_set(:@vstring, 'vMAX') 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_spec.rb b/spec/unit/module_spec.rb index 657ca403a..adabf55b3 100755 --- a/spec/unit/module_spec.rb +++ b/spec/unit/module_spec.rb @@ -1,731 +1,864 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet_spec/files' require 'puppet_spec/modules' require 'puppet/module_tool/checksums' describe Puppet::Module do include PuppetSpec::Files before do # This is necessary because of the extra checks we have for the deprecated # 'plugins' directory FileTest.stubs(:exist?).returns false end it "should have a class method that returns a named module from a given environment" do env = mock 'module' env.expects(:module).with("mymod").returns "yep" Puppet::Node::Environment.expects(:new).with("myenv").returns env Puppet::Module.find("mymod", "myenv").should == "yep" end it "should return nil if asked for a named module that doesn't exist" do env = mock 'module' env.expects(:module).with("mymod").returns nil Puppet::Node::Environment.expects(:new).with("myenv").returns env Puppet::Module.find("mymod", "myenv").should be_nil end it "should support a 'version' attribute" do mod = Puppet::Module.new("mymod") mod.version = 1.09 mod.version.should == 1.09 end it "should support a 'source' attribute" do mod = Puppet::Module.new("mymod") mod.source = "http://foo/bar" mod.source.should == "http://foo/bar" end it "should support a 'project_page' attribute" do mod = Puppet::Module.new("mymod") mod.project_page = "http://foo/bar" mod.project_page.should == "http://foo/bar" end it "should support an 'author' attribute" do mod = Puppet::Module.new("mymod") mod.author = "Luke Kanies " mod.author.should == "Luke Kanies " end it "should support a 'license' attribute" do mod = Puppet::Module.new("mymod") mod.license = "GPL2" mod.license.should == "GPL2" end it "should support a 'summary' attribute" do mod = Puppet::Module.new("mymod") mod.summary = "GPL2" mod.summary.should == "GPL2" end it "should support a 'description' attribute" do mod = Puppet::Module.new("mymod") mod.description = "GPL2" mod.description.should == "GPL2" end it "should support specifying a compatible puppet version" do mod = Puppet::Module.new("mymod") mod.puppetversion = "0.25" mod.puppetversion.should == "0.25" end it "should validate that the puppet version is compatible" do mod = Puppet::Module.new("mymod") mod.puppetversion = "0.25" Puppet.expects(:version).returns "0.25" mod.validate_puppet_version end it "should fail if the specified puppet version is not compatible" do mod = Puppet::Module.new("mymod") mod.puppetversion = "0.25" Puppet.stubs(:version).returns "0.24" lambda { mod.validate_puppet_version }.should raise_error(Puppet::Module::IncompatibleModule) end describe "when finding unmet dependencies" do before do - @mod = Puppet::Module.new("mymod") - @mod.stubs(:dependencies).returns [ - { - "version_requirement" => ">= 2.2.0", - "name" => "baz/foobar" - } - ] + FileTest.unstub(:exist?) + @modpath = tmpdir('modpath') + Puppet.settings[:modulepath] = @modpath end it "should list modules that are missing" do - @mod.unmet_dependencies.should == [{ - :name => 'baz/foobar', - :error => <<-HEREDOC.gsub(/^\s{10}/, '') - Missing dependency `foobar`: - `mymod` () requires `baz/foobar` (>= 2.2.0) - HEREDOC + mod = PuppetSpec::Modules.create( + 'needy', + @modpath, + :metadata => { + :dependencies => [{ + "version_requirement" => ">= 2.2.0", + "name" => "baz/foobar" + }] + } + ) + mod.unmet_dependencies.should == [{ + :reason => :missing, + :name => "baz/foobar", + :version_constraint => ">= 2.2.0", + :parent => { :name => 'puppetlabs/needy', :version => 'v9.9.9' }, + :mod_details => { :installed_version => nil } }] end - it "should list modules with unmet version" do - foobar = Puppet::Module.new("foobar") - foobar.version = '2.0.0' - @mod.environment.expects(:module).with("foobar").returns foobar - - @mod.unmet_dependencies.should == [{ - :name => 'baz/foobar', - :error => <<-HEREDOC.gsub(/^\s{10}/, '') - Version dependency mismatch `foobar` (2.0.0): - `mymod` () requires `baz/foobar` (>= 2.2.0) - HEREDOC + it "should list modules that are missing and have invalid names" do + mod = PuppetSpec::Modules.create( + 'needy', + @modpath, + :metadata => { + :dependencies => [{ + "version_requirement" => ">= 2.2.0", + "name" => "baz/foobar=bar" + }] + } + ) + mod.unmet_dependencies.should == [{ + :reason => :missing, + :name => "baz/foobar=bar", + :version_constraint => ">= 2.2.0", + :parent => { :name => 'puppetlabs/needy', :version => 'v9.9.9' }, + :mod_details => { :installed_version => nil } }] end - it "should consider a dependency without a version requirement to be satisfied" do - mod = Puppet::Module.new("mymod") - mod.stubs(:dependencies).returns [{ "name" => "baz/foobar" }] - - foobar = Puppet::Module.new("foobar") - mod.environment.expects(:module).with("foobar").returns foobar - - mod.unmet_dependencies.should be_empty - end + it "should list modules with unmet version requirement" do + mod = PuppetSpec::Modules.create( + 'foobar', + @modpath, + :metadata => { + :dependencies => [{ + "version_requirement" => ">= 2.2.0", + "name" => "baz/foobar" + }] + } + ) + mod2 = PuppetSpec::Modules.create( + 'foobaz', + @modpath, + :metadata => { + :dependencies => [{ + "version_requirement" => "1.0.0", + "name" => "baz/foobar" + }] + } + ) - it "should consider a dependency without a version to be unmet" do - foobar = Puppet::Module.new("foobar") - @mod.environment.expects(:module).with("foobar").returns foobar + PuppetSpec::Modules.create( + 'foobar', + @modpath, + :metadata => { :version => '2.0.0', :author => 'baz' } + ) - @mod.unmet_dependencies.should == [{ - :name => 'baz/foobar', - :error => <<-HEREDOC.gsub(/^\s{10}/, '') - Unversioned dependency `foobar`: - `mymod` () requires `baz/foobar` (>= 2.2.0) - HEREDOC + mod.unmet_dependencies.should == [{ + :reason => :version_mismatch, + :name => "baz/foobar", + :version_constraint => ">= 2.2.0", + :parent => { :version => "v9.9.9", :name => "puppetlabs/foobar" }, + :mod_details => { :installed_version => "2.0.0" } }] - end - it "should consider a dependency without a semantic version to be unmet" do - foobar = Puppet::Module.new("foobar") - foobar.version = '5.1' - @mod.environment.expects(:module).with("foobar").returns foobar - - @mod.unmet_dependencies.should == [{ - :name => 'baz/foobar', - :error => <<-HEREDOC.gsub(/^\s{10}/, '') - Non semantic version dependency `foobar` (5.1): - `mymod` () requires `baz/foobar` (>= 2.2.0) - HEREDOC + mod2.unmet_dependencies.should == [{ + :reason => :version_mismatch, + :name => "baz/foobar", + :version_constraint => "v1.0.0", + :parent => { :version => "v9.9.9", :name => "puppetlabs/foobaz" }, + :mod_details => { :installed_version => "2.0.0" } }] + end - it "should consider a dependency requirement without a semantic version to be unmet" do - foobar = Puppet::Module.new("foobar") - foobar.version = '5.1.0' + it "should consider a dependency without a version requirement to be satisfied" do + mod = PuppetSpec::Modules.create( + 'foobar', + @modpath, + :metadata => { + :dependencies => [{ + "name" => "baz/foobar" + }] + } + ) + PuppetSpec::Modules.create( + 'foobar', + @modpath, + :metadata => { + :version => '2.0.0', + :author => 'baz' + } + ) + + mod.unmet_dependencies.should be_empty + end - mod = Puppet::Module.new("mymod") - mod.stubs(:dependencies).returns [{ "name" => "baz/foobar", "version_requirement" => '> 2.0' }] - mod.environment.expects(:module).with("foobar").returns foobar + it "should consider a dependency without a semantic version to be unmet" do + mod = PuppetSpec::Modules.create( + 'foobar', + @modpath, + :metadata => { + :dependencies => [{ + "name" => "baz/foobar" + }] + } + ) + PuppetSpec::Modules.create( + 'foobar', + @modpath, + :metadata => { + :version => '5.1', + :author => 'baz' + } + ) mod.unmet_dependencies.should == [{ - :name => 'baz/foobar', - :error => <<-HEREDOC.gsub(/^\s{10}/, '') - Non semantic version dependency `foobar` (5.1.0): - `mymod` () requires `baz/foobar` (> 2.0) - HEREDOC + :reason => :non_semantic_version, + :parent => { :version => "v9.9.9", :name => "puppetlabs/foobar" }, + :mod_details => { :installed_version => "5.1" }, + :name => "baz/foobar", + :version_constraint => ">= 0.0.0" }] end it "should have valid dependencies when no dependencies have been specified" do - mod = Puppet::Module.new("mymod") + mod = PuppetSpec::Modules.create( + 'foobar', + @modpath, + :metadata => { + :dependencies => [] + } + ) mod.unmet_dependencies.should == [] end it "should only list unmet dependencies" do - mod = Puppet::Module.new("mymod") - mod.stubs(:dependencies).returns [ - { - "version_requirement" => ">= 2.2.0", - "name" => "baz/satisfied" - }, - { - "version_requirement" => ">= 2.2.0", - "name" => "baz/notsatisfied" + mod = PuppetSpec::Modules.create( + 'mymod', + @modpath, + :metadata => { + :dependencies => [ + { + "version_requirement" => ">= 2.2.0", + "name" => "baz/satisfied" + }, + { + "version_requirement" => ">= 2.2.0", + "name" => "baz/notsatisfied" + } + ] } - ] - - satisfied = Puppet::Module.new("satisfied") - satisfied.version = "3.3.0" - - mod.environment.expects(:module).with("satisfied").returns satisfied - mod.environment.expects(:module).with("notsatisfied").returns nil + ) + PuppetSpec::Modules.create( + 'satisfied', + @modpath, + :metadata => { + :version => '3.3.0', + :author => 'baz' + } + ) mod.unmet_dependencies.should == [{ - :name => 'baz/notsatisfied', - :error => <<-HEREDOC.gsub(/^\s{10}/, '') - Missing dependency `notsatisfied`: - `mymod` () requires `baz/notsatisfied` (>= 2.2.0) - HEREDOC + :reason => :missing, + :mod_details => { :installed_version => nil }, + :parent => { :version => "v9.9.9", :name => "puppetlabs/mymod" }, + :name => "baz/notsatisfied", + :version_constraint => ">= 2.2.0" }] end it "should be empty when all dependencies are met" do - mod = Puppet::Module.new("mymod") - mod.stubs(:dependencies).returns [ - { - "version_requirement" => ">= 2.2.0", - "name" => "baz/satisfied" - }, - { - "version_requirement" => "< 2.2.0", - "name" => "baz/alsosatisfied" + mod = PuppetSpec::Modules.create( + 'mymod2', + @modpath, + :metadata => { + :dependencies => [ + { + "version_requirement" => ">= 2.2.0", + "name" => "baz/satisfied" + }, + { + "version_requirement" => "< 2.2.0", + "name" => "baz/alsosatisfied" + } + ] } - ] - satisfied = Puppet::Module.new("satisfied") - satisfied.version = "3.3.0" - alsosatisfied = Puppet::Module.new("alsosatisfied") - alsosatisfied.version = "2.1.0" - - mod.environment.expects(:module).with("satisfied").returns satisfied - mod.environment.expects(:module).with("alsosatisfied").returns alsosatisfied + ) + PuppetSpec::Modules.create( + 'satisfied', + @modpath, + :metadata => { + :version => '3.3.0', + :author => 'baz' + } + ) + PuppetSpec::Modules.create( + 'alsosatisfied', + @modpath, + :metadata => { + :version => '2.1.0', + :author => 'baz' + } + ) mod.unmet_dependencies.should be_empty end end describe "when managing supported platforms" do it "should support specifying a supported platform" do mod = Puppet::Module.new("mymod") mod.supports "solaris" end it "should support specifying a supported platform and version" do mod = Puppet::Module.new("mymod") mod.supports "solaris", 1.0 end it "should fail when not running on a supported platform" do pending "Not sure how to send client platform to the module" mod = Puppet::Module.new("mymod") Facter.expects(:value).with("operatingsystem").returns "Solaris" mod.supports "hpux" lambda { mod.validate_supported_platform }.should raise_error(Puppet::Module::UnsupportedPlatform) end it "should fail when supported platforms are present but of the wrong version" do pending "Not sure how to send client platform to the module" mod = Puppet::Module.new("mymod") Facter.expects(:value).with("operatingsystem").returns "Solaris" Facter.expects(:value).with("operatingsystemrelease").returns 2.0 mod.supports "Solaris", 1.0 lambda { mod.validate_supported_platform }.should raise_error(Puppet::Module::IncompatiblePlatform) end it "should be considered supported when no supported platforms have been specified" do pending "Not sure how to send client platform to the module" mod = Puppet::Module.new("mymod") lambda { mod.validate_supported_platform }.should_not raise_error end it "should be considered supported when running on a supported platform" do pending "Not sure how to send client platform to the module" mod = Puppet::Module.new("mymod") Facter.expects(:value).with("operatingsystem").returns "Solaris" Facter.expects(:value).with("operatingsystemrelease").returns 2.0 mod.supports "Solaris", 1.0 lambda { mod.validate_supported_platform }.should raise_error(Puppet::Module::IncompatiblePlatform) end it "should be considered supported when running on any of multiple supported platforms" do pending "Not sure how to send client platform to the module" end it "should validate its platform support on initialization" do pending "Not sure how to send client platform to the module" end end it "should return nil if asked for a module whose name is 'nil'" do Puppet::Module.find(nil, "myenv").should be_nil end it "should provide support for logging" do Puppet::Module.ancestors.should be_include(Puppet::Util::Logging) end it "should be able to be converted to a string" do Puppet::Module.new("foo").to_s.should == "Module foo" end it "should add the path to its string form if the module is found" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a" mod.to_s.should == "Module foo(/a)" end it "should fail if its name is not alphanumeric" do lambda { Puppet::Module.new(".something") }.should raise_error(Puppet::Module::InvalidName) end it "should require a name at initialization" do lambda { Puppet::Module.new }.should raise_error(ArgumentError) end it "should convert an environment name into an Environment instance" do Puppet::Module.new("foo", :environment => "prod").environment.should be_instance_of(Puppet::Node::Environment) end it "should accept an environment at initialization" do Puppet::Module.new("foo", :environment => :prod).environment.name.should == :prod end it "should use the default environment if none is provided" do env = Puppet::Node::Environment.new Puppet::Module.new("foo").environment.should equal(env) end it "should use any provided Environment instance" do env = Puppet::Node::Environment.new Puppet::Module.new("foo", :environment => env).environment.should equal(env) end describe ".path" do before do dir = tmpdir("deep_path") @first = File.join(dir, "first") @second = File.join(dir, "second") Puppet[:modulepath] = "#{@first}#{File::PATH_SEPARATOR}#{@second}" FileUtils.mkdir_p(@first) FileUtils.mkdir_p(@second) end it "should return the path to the first found instance in its environment's module paths as its path" do modpath = File.join(@first, "foo") FileUtils.mkdir_p(modpath) # Make a second one, which we shouldn't find FileUtils.mkdir_p(File.join(@second, "foo")) mod = Puppet::Module.new("foo") mod.path.should == modpath end it "should be able to find itself in a directory other than the first directory in the module path" do modpath = File.join(@second, "foo") FileUtils.mkdir_p(modpath) mod = Puppet::Module.new("foo") mod.should be_exist mod.path.should == modpath end it "should be able to find itself in a directory other than the first directory in the module path even when it exists in the first" do environment = Puppet::Node::Environment.new first_modpath = File.join(@first, "foo") FileUtils.mkdir_p(first_modpath) second_modpath = File.join(@second, "foo") FileUtils.mkdir_p(second_modpath) mod = Puppet::Module.new("foo", :environment => environment, :path => second_modpath) mod.path.should == File.join(@second, "foo") mod.environment.should == environment end end + describe '#modulepath' do + it "should return the directory the module is installed in, if a path exists" do + mod = Puppet::Module.new("foo") + mod.stubs(:path).returns "/a/foo" + mod.modulepath.should == '/a' + end + + it "should return nil if no path exists" do + mod = Puppet::Module.new("foo") + mod.stubs(:path).returns nil + mod.modulepath.should be_nil + end + end + it "should be considered existent if it exists in at least one module path" do mod = Puppet::Module.new("foo") mod.expects(:path).returns "/a/foo" mod.should be_exist end it "should be considered nonexistent if it does not exist in any of the module paths" do mod = Puppet::Module.new("foo") mod.expects(:path).returns nil mod.should_not be_exist end [:plugins, :templates, :files, :manifests].each do |filetype| dirname = filetype == :plugins ? "lib" : filetype.to_s it "should be able to return individual #{filetype}" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" path = File.join("/a/foo", dirname, "my/file") FileTest.expects(:exist?).with(path).returns true mod.send(filetype.to_s.sub(/s$/, ''), "my/file").should == path end it "should consider #{filetype} to be present if their base directory exists" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" path = File.join("/a/foo", dirname) FileTest.expects(:exist?).with(path).returns true mod.send(filetype.to_s + "?").should be_true end it "should consider #{filetype} to be absent if their base directory does not exist" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" path = File.join("/a/foo", dirname) FileTest.expects(:exist?).with(path).returns false mod.send(filetype.to_s + "?").should be_false end it "should consider #{filetype} to be absent if the module base directory does not exist" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns nil mod.send(filetype.to_s + "?").should be_false end it "should return nil if asked to return individual #{filetype} that don't exist" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" path = File.join("/a/foo", dirname, "my/file") FileTest.expects(:exist?).with(path).returns false mod.send(filetype.to_s.sub(/s$/, ''), "my/file").should be_nil end it "should return nil when asked for individual #{filetype} if the module does not exist" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns nil mod.send(filetype.to_s.sub(/s$/, ''), "my/file").should be_nil end it "should return the base directory if asked for a nil path" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" base = File.join("/a/foo", dirname) FileTest.expects(:exist?).with(base).returns true mod.send(filetype.to_s.sub(/s$/, ''), nil).should == base end end %w{plugins files}.each do |filetype| short = filetype.sub(/s$/, '') dirname = filetype == "plugins" ? "lib" : filetype.to_s it "should be able to return the #{short} directory" do Puppet::Module.new("foo").should respond_to(short + "_directory") end it "should return the path to the #{short} directory" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" mod.send(short + "_directory").should == "/a/foo/#{dirname}" end end it "should throw a warning if plugins are in a 'plugins' directory rather than a 'lib' directory" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" FileTest.expects(:exist?).with("/a/foo/plugins").returns true mod.plugin_directory.should == "/a/foo/plugins" @logs.first.message.should == "using the deprecated 'plugins' directory for ruby extensions; please move to 'lib'" @logs.first.level.should == :warning end it "should default to 'lib' for the plugins directory" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" mod.plugin_directory.should == "/a/foo/lib" end end describe Puppet::Module, "when finding matching manifests" do before do @mod = Puppet::Module.new("mymod") @mod.stubs(:path).returns "/a" @pq_glob_with_extension = "yay/*.xx" @fq_glob_with_extension = "/a/manifests/#{@pq_glob_with_extension}" end it "should return all manifests matching the glob pattern" do Dir.expects(:glob).with(@fq_glob_with_extension).returns(%w{foo bar}) FileTest.stubs(:directory?).returns false @mod.match_manifests(@pq_glob_with_extension).should == %w{foo bar} end it "should not return directories" do Dir.expects(:glob).with(@fq_glob_with_extension).returns(%w{foo bar}) FileTest.expects(:directory?).with("foo").returns false FileTest.expects(:directory?).with("bar").returns true @mod.match_manifests(@pq_glob_with_extension).should == %w{foo} end it "should default to the 'init' file if no glob pattern is specified" do Dir.expects(:glob).with("/a/manifests/init.{pp,rb}").returns(%w{/a/manifests/init.pp}) @mod.match_manifests(nil).should == %w{/a/manifests/init.pp} end it "should return all manifests matching the glob pattern in all existing paths" do Dir.expects(:glob).with(@fq_glob_with_extension).returns(%w{a b}) @mod.match_manifests(@pq_glob_with_extension).should == %w{a b} end it "should match the glob pattern plus '.{pp,rb}' if no extention is specified" do Dir.expects(:glob).with("/a/manifests/yay/foo.{pp,rb}").returns(%w{yay}) @mod.match_manifests("yay/foo").should == %w{yay} end it "should return an empty array if no manifests matched" do Dir.expects(:glob).with(@fq_glob_with_extension).returns([]) @mod.match_manifests(@pq_glob_with_extension).should == [] end end describe Puppet::Module do include PuppetSpec::Files before do @modpath = tmpdir('modpath') @module = PuppetSpec::Modules.create('mymod', @modpath) end it "should use 'License' in its current path as its metadata file" do @module.license_file.should == "#{@modpath}/mymod/License" end it "should return nil as its license file when the module has no path" do Puppet::Module.any_instance.stubs(:path).returns nil Puppet::Module.new("foo").license_file.should be_nil end it "should cache the license file" do @module.expects(:path).once.returns nil @module.license_file @module.license_file end it "should use 'metadata.json' in its current path as its metadata file" do @module.metadata_file.should == "#{@modpath}/mymod/metadata.json" end it "should return nil as its metadata file when the module has no path" do Puppet::Module.any_instance.stubs(:path).returns nil Puppet::Module.new("foo").metadata_file.should be_nil end it "should cache the metadata file" do Puppet::Module.any_instance.expects(:path).once.returns nil mod = Puppet::Module.new("foo") mod.metadata_file.should == mod.metadata_file end it "should have metadata if it has a metadata file and its data is not empty" do FileTest.expects(:exist?).with(@module.metadata_file).returns true File.stubs(:read).with(@module.metadata_file).returns "{\"foo\" : \"bar\"}" @module.should be_has_metadata end it "should have metadata if it has a metadata file and its data is not empty" do FileTest.expects(:exist?).with(@module.metadata_file).returns true File.stubs(:read).with(@module.metadata_file).returns "{\"foo\" : \"bar\"}" @module.should be_has_metadata end it "should not have metadata if has a metadata file and its data is empty" do FileTest.expects(:exist?).with(@module.metadata_file).returns true File.stubs(:read).with(@module.metadata_file).returns "/* +-----------------------------------------------------------------------+ | | | ==> DO NOT EDIT THIS FILE! <== | | | | You should edit the `Modulefile` and run `puppet-module build` | | to generate the `metadata.json` file for your releases. | | | +-----------------------------------------------------------------------+ */ {}" @module.should_not be_has_metadata end it "should know if it is missing a metadata file" do FileTest.expects(:exist?).with(@module.metadata_file).returns false @module.should_not be_has_metadata end it "should be able to parse its metadata file" do @module.should respond_to(:load_metadata) end it "should parse its metadata file on initialization if it is present" do Puppet::Module.any_instance.expects(:has_metadata?).returns true Puppet::Module.any_instance.expects(:load_metadata) Puppet::Module.new("yay") end describe "when loading the metadata file", :if => Puppet.features.pson? do before do @data = { :license => "GPL2", :author => "luke", :version => "1.0", :source => "http://foo/", :puppetversion => "0.25", :dependencies => [] } @text = @data.to_pson @module = Puppet::Module.new("foo") @module.stubs(:metadata_file).returns "/my/file" File.stubs(:read).with("/my/file").returns @text end %w{source author version license}.each do |attr| it "should set #{attr} if present in the metadata file" do @module.load_metadata @module.send(attr).should == @data[attr.to_sym] end it "should fail if #{attr} is not present in the metadata file" do @data.delete(attr.to_sym) @text = @data.to_pson File.stubs(:read).with("/my/file").returns @text lambda { @module.load_metadata }.should raise_error( Puppet::Module::MissingMetadata, "No #{attr} module metadata provided for foo" ) end end it "should set puppetversion if present in the metadata file" do @module.load_metadata @module.puppetversion.should == @data[:puppetversion] end + context "when versionRequirement is used for dependency version info" do + before do + @data = { + :license => "GPL2", + :author => "luke", + :version => "1.0", + :source => "http://foo/", + :puppetversion => "0.25", + :dependencies => [ + { + "versionRequirement" => "0.0.1", + "name" => "pmtacceptance/stdlib" + }, + { + "versionRequirement" => "0.1.0", + "name" => "pmtacceptance/apache" + } + ] + } + @text = @data.to_pson + + @module = Puppet::Module.new("foo") + @module.stubs(:metadata_file).returns "/my/file" + File.stubs(:read).with("/my/file").returns @text + end + + it "should set the dependency version_requirement key" do + @module.load_metadata + @module.dependencies[0]['version_requirement'].should == "0.0.1" + end + + it "should set the version_requirement key for all dependencies" do + @module.load_metadata + @module.dependencies[0]['version_requirement'].should == "0.0.1" + @module.dependencies[1]['version_requirement'].should == "0.1.0" + end + end + it "should fail if the discovered name is different than the metadata name" end - it "should be able to tell if there are local changes" do + it "should be able to tell if there are local changes", :fails_on_windows => true do modpath = tmpdir('modpath') foo_checksum = 'acbd18db4cc2f85cedef654fccc4a4d8' checksummed_module = PuppetSpec::Modules.create( 'changed', modpath, :metadata => { :checksums => { "foo" => foo_checksum, } } ) foo_path = Pathname.new(File.join(checksummed_module.path, 'foo')) IO.binwrite(foo_path, 'notfoo') Puppet::Module::Tool::Checksums.new(foo_path).checksum(foo_path).should_not == foo_checksum checksummed_module.has_local_changes?.should be_true IO.binwrite(foo_path, 'foo') Puppet::Module::Tool::Checksums.new(foo_path).checksum(foo_path).should == foo_checksum checksummed_module.has_local_changes?.should be_false end it "should know what other modules require it" do Puppet.settings[:modulepath] = @modpath dependable = PuppetSpec::Modules.create( 'dependable', @modpath, :metadata => {:author => 'puppetlabs'} ) PuppetSpec::Modules.create( 'needy', @modpath, :metadata => { :author => 'beggar', :dependencies => [{ "version_requirement" => ">= 2.2.0", "name" => "puppetlabs/dependable" }] } ) PuppetSpec::Modules.create( 'wantit', @modpath, :metadata => { :author => 'spoiled', :dependencies => [{ "version_requirement" => "< 5.0.0", "name" => "puppetlabs/dependable" }] } ) dependable.required_by.should =~ [ { "name" => "beggar/needy", "version" => "9.9.9", "version_requirement" => ">= 2.2.0" }, { "name" => "spoiled/wantit", "version" => "9.9.9", "version_requirement" => "< 5.0.0" } ] 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/node/environment_spec.rb b/spec/unit/node/environment_spec.rb index 1af6151e1..62aef593a 100755 --- a/spec/unit/node/environment_spec.rb +++ b/spec/unit/node/environment_spec.rb @@ -1,432 +1,444 @@ #!/usr/bin/env rspec require 'spec_helper' require 'tmpdir' require 'puppet/node/environment' require 'puppet/util/execution' require 'puppet_spec/modules' describe Puppet::Node::Environment do let(:env) { Puppet::Node::Environment.new("testing") } include PuppetSpec::Files after do Puppet::Node::Environment.clear end it "should use the filetimeout for the ttl for the modulepath" do Puppet::Node::Environment.attr_ttl(:modulepath).should == Integer(Puppet[:filetimeout]) end it "should use the filetimeout for the ttl for the module list" do Puppet::Node::Environment.attr_ttl(:modules).should == Integer(Puppet[:filetimeout]) end it "should use the default environment if no name is provided while initializing an environment" do Puppet.settings.expects(:value).with(:environment).returns("one") Puppet::Node::Environment.new.name.should == :one end it "should treat environment instances as singletons" do Puppet::Node::Environment.new("one").should equal(Puppet::Node::Environment.new("one")) end it "should treat an environment specified as names or strings as equivalent" do Puppet::Node::Environment.new(:one).should equal(Puppet::Node::Environment.new("one")) end it "should return its name when converted to a string" do Puppet::Node::Environment.new(:one).to_s.should == "one" end it "should just return any provided environment if an environment is provided as the name" do one = Puppet::Node::Environment.new(:one) Puppet::Node::Environment.new(one).should equal(one) end describe "when managing known resource types" do before do @collection = Puppet::Resource::TypeCollection.new(env) env.stubs(:perform_initial_import).returns(Puppet::Parser::AST::Hostclass.new('')) Thread.current[:known_resource_types] = nil end it "should create a resource type collection if none exists" do Puppet::Resource::TypeCollection.expects(:new).with(env).returns @collection env.known_resource_types.should equal(@collection) end it "should reuse any existing resource type collection" do env.known_resource_types.should equal(env.known_resource_types) end it "should perform the initial import when creating a new collection" do env.expects(:perform_initial_import).returns(Puppet::Parser::AST::Hostclass.new('')) env.known_resource_types end it "should return the same collection even if stale if it's the same thread" do Puppet::Resource::TypeCollection.stubs(:new).returns @collection env.known_resource_types.stubs(:stale?).returns true env.known_resource_types.should equal(@collection) end it "should return the current thread associated collection if there is one" do Thread.current[:known_resource_types] = @collection env.known_resource_types.should equal(@collection) end it "should give to all threads using the same environment the same collection if the collection isn't stale" do original_thread_type_collection = Puppet::Resource::TypeCollection.new(env) Puppet::Resource::TypeCollection.expects(:new).with(env).returns original_thread_type_collection env.known_resource_types.should equal(original_thread_type_collection) original_thread_type_collection.expects(:require_reparse?).returns(false) Puppet::Resource::TypeCollection.stubs(:new).with(env).returns @collection t = Thread.new { env.known_resource_types.should equal(original_thread_type_collection) } t.join end it "should generate a new TypeCollection if the current one requires reparsing" do old_type_collection = env.known_resource_types old_type_collection.stubs(:require_reparse?).returns true Thread.current[:known_resource_types] = nil new_type_collection = env.known_resource_types new_type_collection.should be_a Puppet::Resource::TypeCollection new_type_collection.should_not equal(old_type_collection) end end it "should validate the modulepath directories" do real_file = tmpdir('moduledir') path = %W[/one /two #{real_file}].join(File::PATH_SEPARATOR) Puppet[:modulepath] = path env.modulepath.should == [real_file] end it "should prefix the value of the 'PUPPETLIB' environment variable to the module path if present" do Puppet::Util::Execution.withenv("PUPPETLIB" => %w{/l1 /l2}.join(File::PATH_SEPARATOR)) do module_path = %w{/one /two}.join(File::PATH_SEPARATOR) env.expects(:validate_dirs).with(%w{/l1 /l2 /one /two}).returns %w{/l1 /l2 /one /two} env.expects(:[]).with(:modulepath).returns module_path env.modulepath.should == %w{/l1 /l2 /one /two} end end describe "when validating modulepath or manifestdir directories" do before :each do @path_one = tmpdir("path_one") @path_two = tmpdir("path_one") sep = File::PATH_SEPARATOR Puppet[:modulepath] = "#{@path_one}#{sep}#{@path_two}" end it "should not return non-directories" do FileTest.expects(:directory?).with(@path_one).returns true FileTest.expects(:directory?).with(@path_two).returns false env.validate_dirs([@path_one, @path_two]).should == [@path_one] end it "should use the current working directory to fully-qualify unqualified paths" do FileTest.stubs(:directory?).returns true two = File.expand_path(File.join(Dir.getwd, "two")) env.validate_dirs([@path_one, 'two']).should == [@path_one, two] end end describe "when modeling a specific environment" do it "should have a method for returning the environment name" do Puppet::Node::Environment.new("testing").name.should == :testing end it "should provide an array-like accessor method for returning any environment-specific setting" do env.should respond_to(:[]) end it "should ask the Puppet settings instance for the setting qualified with the environment name" do Puppet.settings.expects(:value).with("myvar", :testing).returns("myval") env["myvar"].should == "myval" end it "should be able to return an individual module that exists in its module path" do mod = mock 'module' Puppet::Module.expects(:new).with("one", :environment => env).returns mod mod.expects(:exist?).returns true env.module("one").should equal(mod) end it "should return nil if asked for a module that does not exist in its path" do modpath = tmpdir('modpath') env.modulepath = [modpath] env.module("one").should be_nil end describe "module data" do before do dir = tmpdir("deep_path") @first = File.join(dir, "first") @second = File.join(dir, "second") Puppet[:modulepath] = "#{@first}#{File::PATH_SEPARATOR}#{@second}" FileUtils.mkdir_p(@first) FileUtils.mkdir_p(@second) end describe "#modules_by_path" do it "should return an empty list if there are no modules" do env.modules_by_path.should == { @first => [], @second => [] } end it "should include modules even if they exist in multiple dirs in the modulepath" do modpath1 = File.join(@first, "foo") FileUtils.mkdir_p(modpath1) modpath2 = File.join(@second, "foo") FileUtils.mkdir_p(modpath2) env.modules_by_path.should == { @first => [Puppet::Module.new('foo', :environment => env, :path => modpath1)], @second => [Puppet::Module.new('foo', :environment => env, :path => modpath2)] } end + + it "should ignore modules with invalid names" do + FileUtils.mkdir_p(File.join(@first, 'foo')) + FileUtils.mkdir_p(File.join(@first, 'foo2')) + FileUtils.mkdir_p(File.join(@first, 'foo-bar')) + FileUtils.mkdir_p(File.join(@first, 'foo_bar')) + FileUtils.mkdir_p(File.join(@first, 'foo=bar')) + FileUtils.mkdir_p(File.join(@first, 'foo bar')) + FileUtils.mkdir_p(File.join(@first, 'foo.bar')) + + env.modules_by_path[@first].collect{|mod| mod.name}.sort.should == %w{foo foo-bar foo2 foo_bar} + end + end describe "#module_requirements" do it "should return a list of what modules depend on other modules" do PuppetSpec::Modules.create( 'foo', @first, :metadata => { :author => 'puppetlabs', :dependencies => [{ 'name' => 'puppetlabs/bar', "version_requirement" => ">= 1.0.0" }] } ) PuppetSpec::Modules.create( 'bar', @second, :metadata => { :author => 'puppetlabs', :dependencies => [{ 'name' => 'puppetlabs/foo', "version_requirement" => "<= 2.0.0" }] } ) PuppetSpec::Modules.create( 'baz', @first, :metadata => { :author => 'puppetlabs', :dependencies => [{ 'name' => 'puppetlabs/bar', "version_requirement" => "3.0.0" }] } ) PuppetSpec::Modules.create( 'alpha', @first, :metadata => { :author => 'puppetlabs', :dependencies => [{ 'name' => 'puppetlabs/bar', "version_requirement" => "~3.0.0" }] } ) env.module_requirements.should == { 'puppetlabs/alpha' => [], 'puppetlabs/foo' => [ { "name" => "puppetlabs/bar", "version" => "9.9.9", "version_requirement" => "<= 2.0.0" } ], 'puppetlabs/bar' => [ { "name" => "puppetlabs/alpha", "version" => "9.9.9", "version_requirement" => "~3.0.0" }, { "name" => "puppetlabs/baz", "version" => "9.9.9", "version_requirement" => "3.0.0" }, { "name" => "puppetlabs/foo", "version" => "9.9.9", "version_requirement" => ">= 1.0.0" } ], 'puppetlabs/baz' => [] } end end describe ".module_by_forge_name" do it "should find modules by forge_name" do mod = PuppetSpec::Modules.create( 'baz', @first, :metadata => {:author => 'puppetlabs'}, :environment => env ) env.module_by_forge_name('puppetlabs/baz').should == mod end it "should not find modules with same name by the wrong author" do mod = PuppetSpec::Modules.create( 'baz', @first, :metadata => {:author => 'sneakylabs'}, :environment => env ) env.module_by_forge_name('puppetlabs/baz').should == nil end it "should return nil when the module can't be found" do env.module_by_forge_name('ima/nothere').should be_nil end end describe ".modules" do it "should return an empty list if there are no modules" do env.modules.should == [] end it "should return a module named for every directory in each module path" do %w{foo bar}.each do |mod_name| FileUtils.mkdir_p(File.join(@first, mod_name)) end %w{bee baz}.each do |mod_name| FileUtils.mkdir_p(File.join(@second, mod_name)) end env.modules.collect{|mod| mod.name}.sort.should == %w{foo bar bee baz}.sort end it "should remove duplicates" do FileUtils.mkdir_p(File.join(@first, 'foo')) FileUtils.mkdir_p(File.join(@second, 'foo')) env.modules.collect{|mod| mod.name}.sort.should == %w{foo} end it "should ignore modules with invalid names" do FileUtils.mkdir_p(File.join(@first, 'foo')) FileUtils.mkdir_p(File.join(@first, 'foo2')) FileUtils.mkdir_p(File.join(@first, 'foo-bar')) FileUtils.mkdir_p(File.join(@first, 'foo_bar')) FileUtils.mkdir_p(File.join(@first, 'foo=bar')) FileUtils.mkdir_p(File.join(@first, 'foo bar')) env.modules.collect{|mod| mod.name}.sort.should == %w{foo foo-bar foo2 foo_bar} end it "should create modules with the correct environment" do FileUtils.mkdir_p(File.join(@first, 'foo')) - env.modules.each {|mod| mod.environment.should == env } end end end it "should cache the module list" do env.modulepath = %w{/a} Dir.expects(:entries).once.with("/a").returns %w{foo} env.modules env.modules end end describe Puppet::Node::Environment::Helper do before do @helper = Object.new @helper.extend(Puppet::Node::Environment::Helper) end it "should be able to set and retrieve the environment as a symbol" do @helper.environment = :foo @helper.environment.name.should == :foo end it "should accept an environment directly" do @helper.environment = Puppet::Node::Environment.new(:foo) @helper.environment.name.should == :foo end it "should accept an environment as a string" do @helper.environment = 'foo' @helper.environment.name.should == :foo end end describe "when performing initial import" do before do @parser = Puppet::Parser::Parser.new("test") Puppet::Parser::Parser.stubs(:new).returns @parser end it "should set the parser's string to the 'code' setting and parse if code is available" do Puppet.settings[:code] = "my code" @parser.expects(:string=).with "my code" @parser.expects(:parse) env.instance_eval { perform_initial_import } end it "should set the parser's file to the 'manifest' setting and parse if no code is available and the manifest is available" do filename = tmpfile('myfile') File.open(filename, 'w'){|f| } Puppet.settings[:manifest] = filename @parser.expects(:file=).with filename @parser.expects(:parse) env.instance_eval { perform_initial_import } end it "should pass the manifest file to the parser even if it does not exist on disk" do filename = tmpfile('myfile') Puppet.settings[:code] = "" Puppet.settings[:manifest] = filename @parser.expects(:file=).with(filename).once @parser.expects(:parse).once env.instance_eval { perform_initial_import } end it "should fail helpfully if there is an error importing" do File.stubs(:exist?).returns true env.stubs(:known_resource_types).returns Puppet::Resource::TypeCollection.new(env) @parser.expects(:file=).once @parser.expects(:parse).raises ArgumentError lambda { env.instance_eval { perform_initial_import } }.should raise_error(Puppet::Error) end it "should not do anything if the ignore_import settings is set" do Puppet.settings[:ignoreimport] = true @parser.expects(:string=).never @parser.expects(:file=).never @parser.expects(:parse).never env.instance_eval { perform_initial_import } end it "should mark the type collection as needing a reparse when there is an error parsing" do @parser.expects(:parse).raises Puppet::ParseError.new("Syntax error at ...") env.stubs(:known_resource_types).returns Puppet::Resource::TypeCollection.new(env) lambda { env.instance_eval { perform_initial_import } }.should raise_error(Puppet::Error, /Syntax error at .../) env.known_resource_types.require_reparse?.should be_true end end end diff --git a/spec/unit/semver_spec.rb b/spec/unit/semver_spec.rb index 87fc968ba..8da6324fb 100644 --- a/spec/unit/semver_spec.rb +++ b/spec/unit/semver_spec.rb @@ -1,280 +1,288 @@ require 'spec_helper' require 'semver' describe SemVer do describe '::valid?' do it 'should validate basic version strings' do %w[ 0.0.0 999.999.999 v0.0.0 v999.999.999 ].each do |vstring| SemVer.valid?(vstring).should be_true end end it 'should validate special version strings' do %w[ 0.0.0-foo 999.999.999-bar v0.0.0-a v999.999.999-beta ].each do |vstring| SemVer.valid?(vstring).should be_true end end it 'should fail to validate invalid version strings' do %w[ nope 0.0foo 999.999 x0.0.0 z.z.z 1.2.3beta 1.x.y ].each do |vstring| SemVer.valid?(vstring).should be_false end end end describe '::find_matching' do before :all do @versions = %w[ 0.0.1 0.0.2 1.0.0-rc1 1.0.0-rc2 1.0.0 1.0.1 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.2.0 1.2.1 2.0.0-rc1 ].map { |v| SemVer.new(v) } end it 'should match exact versions by string' do @versions.each do |version| SemVer.find_matching(version, @versions).should == version end end it 'should return nil if no versions match' do %w[ 3.0.0 2.0.0-rc2 1.0.0-alpha ].each do |v| SemVer.find_matching(v, @versions).should be_nil end end it 'should find the greatest match for partial versions' do SemVer.find_matching('1.0', @versions).should == 'v1.0.1' SemVer.find_matching('1.1', @versions).should == 'v1.1.4' SemVer.find_matching('1', @versions).should == 'v1.2.1' SemVer.find_matching('2', @versions).should == 'v2.0.0-rc1' SemVer.find_matching('2.1', @versions).should == nil end it 'should find the greatest match for versions with placeholders' do SemVer.find_matching('1.0.x', @versions).should == 'v1.0.1' SemVer.find_matching('1.1.x', @versions).should == 'v1.1.4' SemVer.find_matching('1.x', @versions).should == 'v1.2.1' SemVer.find_matching('1.x.x', @versions).should == 'v1.2.1' SemVer.find_matching('2.x', @versions).should == 'v2.0.0-rc1' SemVer.find_matching('2.x.x', @versions).should == 'v2.0.0-rc1' SemVer.find_matching('2.1.x', @versions).should == nil end end describe '::[]' do it "should produce expected ranges" do tests = { - '1.2.3' => SemVer.new('v1.2.3-') .. SemVer.new('v1.2.3'), - '>1.2.3' => SemVer.new('v1.2.4-') .. SemVer::MAX, - '<1.2.3' => SemVer::MIN ... SemVer.new('v1.2.3-'), - '>=1.2.3' => SemVer.new('v1.2.3-') .. SemVer::MAX, - '<=1.2.3' => SemVer::MIN .. SemVer.new('v1.2.3'), - '>1.2.3 <1.2.5' => SemVer.new('v1.2.4-') ... SemVer.new('v1.2.5-'), - '>=1.2.3 <=1.2.5' => SemVer.new('v1.2.3-') .. SemVer.new('v1.2.5'), - '1.2.3 - 2.3.4' => SemVer.new('v1.2.3-') .. SemVer.new('v2.3.4'), - '~1.2.3' => SemVer.new('v1.2.3-') ... SemVer.new('v1.3.0-'), - '~1.2' => SemVer.new('v1.2.0-') ... SemVer.new('v2.0.0-'), - '~1' => SemVer.new('v1.0.0-') ... SemVer.new('v2.0.0-'), - '1.2.x' => SemVer.new('v1.2.0') ... SemVer.new('v1.3.0-'), - '1.x' => SemVer.new('v1.0.0') ... SemVer.new('v2.0.0-'), + '1.2.3-alpha' => SemVer.new('v1.2.3-alpha') .. SemVer.new('v1.2.3-alpha'), + '1.2.3' => SemVer.new('v1.2.3-') .. SemVer.new('v1.2.3'), + '>1.2.3-alpha' => SemVer.new('v1.2.3-alpha-') .. SemVer::MAX, + '>1.2.3' => SemVer.new('v1.2.4-') .. SemVer::MAX, + '<1.2.3-alpha' => SemVer::MIN ... SemVer.new('v1.2.3-alpha'), + '<1.2.3' => SemVer::MIN ... SemVer.new('v1.2.3-'), + '>=1.2.3-alpha' => SemVer.new('v1.2.3-alpha') .. SemVer::MAX, + '>=1.2.3' => SemVer.new('v1.2.3-') .. SemVer::MAX, + '<=1.2.3-alpha' => SemVer::MIN .. SemVer.new('v1.2.3-alpha'), + '<=1.2.3' => SemVer::MIN .. SemVer.new('v1.2.3'), + '>1.2.3-a <1.2.3-b' => SemVer.new('v1.2.3-a-') ... SemVer.new('v1.2.3-b'), + '>1.2.3 <1.2.5' => SemVer.new('v1.2.4-') ... SemVer.new('v1.2.5-'), + '>=1.2.3-a <= 1.2.3-b' => SemVer.new('v1.2.3-a') .. SemVer.new('v1.2.3-b'), + '>=1.2.3 <=1.2.5' => SemVer.new('v1.2.3-') .. SemVer.new('v1.2.5'), + '1.2.3-a - 2.3.4-b' => SemVer.new('v1.2.3-a') .. SemVer.new('v2.3.4-b'), + '1.2.3 - 2.3.4' => SemVer.new('v1.2.3-') .. SemVer.new('v2.3.4'), + '~1.2.3' => SemVer.new('v1.2.3-') ... SemVer.new('v1.3.0-'), + '~1.2' => SemVer.new('v1.2.0-') ... SemVer.new('v2.0.0-'), + '~1' => SemVer.new('v1.0.0-') ... SemVer.new('v2.0.0-'), + '1.2.x' => SemVer.new('v1.2.0') ... SemVer.new('v1.3.0-'), + '1.x' => SemVer.new('v1.0.0') ... SemVer.new('v2.0.0-'), } tests.each do |vstring, expected| SemVer[vstring].should == expected end end it "should suit up" do suitability = { [ '1.2.3', 'v1.2.2' ] => false, [ '>=1.2.3', 'v1.2.2' ] => false, [ '<=1.2.3', 'v1.2.2' ] => true, [ '>= 1.2.3', 'v1.2.2' ] => false, [ '<= 1.2.3', 'v1.2.2' ] => true, [ '1.2.3 - 1.2.4', 'v1.2.2' ] => false, [ '~1.2.3', 'v1.2.2' ] => false, [ '~1.2', 'v1.2.2' ] => true, [ '~1', 'v1.2.2' ] => true, [ '1.2.x', 'v1.2.2' ] => true, [ '1.x', 'v1.2.2' ] => true, [ '1.2.3', 'v1.2.3-alpha' ] => true, [ '>=1.2.3', 'v1.2.3-alpha' ] => true, [ '<=1.2.3', 'v1.2.3-alpha' ] => true, [ '>= 1.2.3', 'v1.2.3-alpha' ] => true, [ '<= 1.2.3', 'v1.2.3-alpha' ] => true, [ '>1.2.3', 'v1.2.3-alpha' ] => false, [ '<1.2.3', 'v1.2.3-alpha' ] => false, [ '> 1.2.3', 'v1.2.3-alpha' ] => false, [ '< 1.2.3', 'v1.2.3-alpha' ] => false, [ '1.2.3 - 1.2.4', 'v1.2.3-alpha' ] => true, [ '1.2.3 - 1.2.4', 'v1.2.4-alpha' ] => true, [ '1.2.3 - 1.2.4', 'v1.2.5-alpha' ] => false, [ '~1.2.3', 'v1.2.3-alpha' ] => true, [ '~1.2.3', 'v1.3.0-alpha' ] => false, [ '~1.2', 'v1.2.3-alpha' ] => true, [ '~1.2', 'v2.0.0-alpha' ] => false, [ '~1', 'v1.2.3-alpha' ] => true, [ '~1', 'v2.0.0-alpha' ] => false, [ '1.2.x', 'v1.2.3-alpha' ] => true, [ '1.2.x', 'v1.3.0-alpha' ] => false, [ '1.x', 'v1.2.3-alpha' ] => true, [ '1.x', 'v2.0.0-alpha' ] => false, [ '1.2.3', 'v1.2.3' ] => true, [ '>=1.2.3', 'v1.2.3' ] => true, [ '<=1.2.3', 'v1.2.3' ] => true, [ '>= 1.2.3', 'v1.2.3' ] => true, [ '<= 1.2.3', 'v1.2.3' ] => true, [ '1.2.3 - 1.2.4', 'v1.2.3' ] => true, [ '~1.2.3', 'v1.2.3' ] => true, [ '~1.2', 'v1.2.3' ] => true, [ '~1', 'v1.2.3' ] => true, [ '1.2.x', 'v1.2.3' ] => true, [ '1.x', 'v1.2.3' ] => true, [ '1.2.3', 'v1.2.4' ] => false, [ '>=1.2.3', 'v1.2.4' ] => true, [ '<=1.2.3', 'v1.2.4' ] => false, [ '>= 1.2.3', 'v1.2.4' ] => true, [ '<= 1.2.3', 'v1.2.4' ] => false, [ '1.2.3 - 1.2.4', 'v1.2.4' ] => true, [ '~1.2.3', 'v1.2.4' ] => true, [ '~1.2', 'v1.2.4' ] => true, [ '~1', 'v1.2.4' ] => true, [ '1.2.x', 'v1.2.4' ] => true, [ '1.x', 'v1.2.4' ] => true, } suitability.each do |arguments, expected| range, vstring = arguments actual = SemVer[range] === SemVer.new(vstring) actual.should == expected end end end describe 'instantiation' do it 'should raise an exception when passed an invalid version string' do expect { SemVer.new('invalidVersion') }.to raise_exception ArgumentError end it 'should populate the appropriate fields for a basic version string' do version = SemVer.new('1.2.3') version.major.should == 1 version.minor.should == 2 version.tiny.should == 3 version.special.should == '' end it 'should populate the appropriate fields for a special version string' do version = SemVer.new('3.4.5-beta6') version.major.should == 3 version.minor.should == 4 version.tiny.should == 5 version.special.should == '-beta6' end end describe '#matched_by?' do subject { SemVer.new('v1.2.3-beta') } describe 'should match against' do describe 'literal version strings' do it { should be_matched_by('1.2.3-beta') } it { should_not be_matched_by('1.2.3-alpha') } it { should_not be_matched_by('1.2.4-beta') } it { should_not be_matched_by('1.3.3-beta') } it { should_not be_matched_by('2.2.3-beta') } end describe 'partial version strings' do it { should be_matched_by('1.2.3') } it { should be_matched_by('1.2') } it { should be_matched_by('1') } end describe 'version strings with placeholders' do it { should be_matched_by('1.2.x') } it { should be_matched_by('1.x.3') } it { should be_matched_by('1.x.x') } it { should be_matched_by('1.x') } end end end describe 'comparisons' do describe 'against a string' do it 'should just work' do SemVer.new('1.2.3').should == '1.2.3' end end describe 'against a symbol' do it 'should just work' do SemVer.new('1.2.3').should == :'1.2.3' end end describe 'on a basic version (v1.2.3)' do subject { SemVer.new('v1.2.3') } it { should == SemVer.new('1.2.3') } # Different major versions it { should > SemVer.new('0.2.3') } it { should < SemVer.new('2.2.3') } # Different minor versions it { should > SemVer.new('1.1.3') } it { should < SemVer.new('1.3.3') } # Different tiny versions it { should > SemVer.new('1.2.2') } it { should < SemVer.new('1.2.4') } # Against special versions it { should > SemVer.new('1.2.3-beta') } it { should < SemVer.new('1.2.4-beta') } end describe 'on a special version (v1.2.3-beta)' do subject { SemVer.new('v1.2.3-beta') } it { should == SemVer.new('1.2.3-beta') } # Same version, final release it { should < SemVer.new('1.2.3') } # Different major versions it { should > SemVer.new('0.2.3') } it { should < SemVer.new('2.2.3') } # Different minor versions it { should > SemVer.new('1.1.3') } it { should < SemVer.new('1.3.3') } # Different tiny versions it { should > SemVer.new('1.2.2') } it { should < SemVer.new('1.2.4') } # Against special versions it { should > SemVer.new('1.2.3-alpha') } it { should < SemVer.new('1.2.3-beta2') } 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