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..87c14a8d1 --- /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", 2012 + 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..c3e0754eb 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.build_tree(return_value[:installed_modules], return_value[:install_dir]) + return_value[:install_dir] + "\n" + + Puppet::Module::Tool.format_tree(tree) + end end end end diff --git a/lib/puppet/face/module/list.rb b/lib/puppet/face/module/list.rb index 7162dfe46..e3acbd9ae 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_build_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_build_node(mod, path, :label_unmet => false, + :path => path, :label_invalid => true) + end end + + output << Puppet::Module::Tool.format_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_build_tree(list, ancestors=[], parent=nil, params={}) + list.map do |mod| + next if @seen[(mod.forge_name or mod.name)] + node = list_build_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_build_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_build_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..e62a7c535 --- /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.build_tree(return_value[:affected_modules], return_value[:base_dir]) + return_value[:base_dir] + "\n" + + Puppet::Module::Tool.format_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 c29939911..5bfb1306d 100644 --- a/lib/puppet/module.rb +++ b/lib/puppet/module.rb @@ -1,268 +1,319 @@ 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 unless path Puppet.debug("No #{type} found; path not specified") return false end type_subpath = subpath(type) unless FileTest.exist?(type_subpath) Puppet.debug("No #{type} found in subpath '#{type_subpath}' " + "(file / directory does not exist)") return false end 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 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.deprecation_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..a05ac8e3a 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.format_tree(nodes, level = 0) + str = '' + nodes.each_with_index do |node, i| + last_node = nodes.length - 1 == i + deps = node[:dependencies] || [] + + str << (indent = " " * level) + str << (last_node ? "└" : "├") + str << "─" + str << (deps.empty? ? "─" : "┬") + str << " #{node[:text]}\n" + + branch = format_tree(deps, level + 1) + branch.gsub!(/^#{indent} /, indent + '│') unless last_node + str << branch + end + + return str + end + + def self.build_tree(mods, dir) + mods.each do |mod| + version_string = mod[:version][:vstring].sub(/^(?!v)/, 'v') + + if mod[:action] == :upgrade + previous_version = mod[:previous_version].sub(/^(?!v)/, 'v') + version_string = "#{previous_version} -> #{version_string}" + end + + mod[:text] = "#{mod[:module]} (#{colorize(:cyan, version_string)})" + mod[:text] += " [#{mod[:path]}]" unless mod[:path] == dir + build_tree(mod[:dependencies], dir) + end + end 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 fc36c357d..aca3c6bfb 100644 --- a/lib/puppet/node/environment.rb +++ b/lib/puppet/node/environment.rb @@ -1,223 +1,225 @@ 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 do |path| module_names = Dir.entries(path) Puppet.debug("Warning: Found directory named 'lib' in module path ('#{path}/lib'); unless " + "you are expecting to load a module named 'lib', your module path may be set " + "incorrectly.") if module_names.include?("lib") module_names end .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/parser/functions/create_resources.rb b/lib/puppet/parser/functions/create_resources.rb index 3c91b4111..9a5304dec 100644 --- a/lib/puppet/parser/functions/create_resources.rb +++ b/lib/puppet/parser/functions/create_resources.rb @@ -1,75 +1,76 @@ Puppet::Parser::Functions::newfunction(:create_resources, :doc => <<-'ENDHEREDOC') do |args| Converts a hash into a set of resources and adds them to the catalog. This function takes two mandatory arguments: a resource type, and a hash describing a set of resources. The hash should be in the form `{title => {parameters} }`: # A hash of user resources: $myusers = { 'nick' => { uid => '1330', group => allstaff, groups => ['developers', 'operations', 'release'], } 'dan' => { uid => '1308', group => allstaff, groups => ['developers', 'prosvc', 'release'], } } create_resources(user, $myusers) A third, optional parameter may be given, also as a hash: $defaults => { 'ensure' => present, 'provider' => 'ldap', } create_resources(user, $myusers, $defaults) The values given on the third argument are added to the parameters of each resource present in the set given on the second argument. If a parameter is present on both the second and third arguments, the one on the second argument takes precedence. This function can be used to create defined resources and classes, as well as native resources. ENDHEREDOC raise ArgumentError, ("create_resources(): wrong number of arguments (#{args.length}; must be 2 or 3)") if args.length < 2 || args.length > 3 # figure out what kind of resource we are type_of_resource = nil type_name = args[0].downcase if type_name == 'class' type_of_resource = :class else if resource = Puppet::Type.type(type_name.to_sym) type_of_resource = :type elsif resource = find_definition(type_name.downcase) type_of_resource = :define else raise ArgumentError, "could not create resource of unknown type #{type_name}" end end # iterate through the resources to create defaults = args[2] || {} args[1].each do |title, params| - raise ArgumentError, 'params should not contain title' if(params['title']) params = defaults.merge(params) + Puppet::Util.symbolizehash!(params) + raise ArgumentError, 'params should not contain title' if(params[:title]) case type_of_resource # JJM The only difference between a type and a define is the call to instantiate_resource # for a defined type. when :type, :define p_resource = Puppet::Parser::Resource.new(type_name, title, :scope => self, :source => resource) - params.merge(:name => title).each do |k,v| + {:name => title}.merge(params).each do |k,v| p_resource.set_parameter(k,v) end if type_of_resource == :define then resource.instantiate_resource(self, p_resource) end compiler.add_resource(self, p_resource) when :class klass = find_hostclass(title) raise ArgumentError, "could not find hostclass #{title}" unless klass klass.ensure_in_catalog(self, params) compiler.catalog.add_class(title) end end end diff --git a/lib/puppet/provider/nameservice/directoryservice.rb b/lib/puppet/provider/nameservice/directoryservice.rb index 083c9a60e..5281d59a3 100644 --- a/lib/puppet/provider/nameservice/directoryservice.rb +++ b/lib/puppet/provider/nameservice/directoryservice.rb @@ -1,592 +1,598 @@ require 'puppet' require 'puppet/provider/nameservice' require 'facter/util/plist' require 'fileutils' class Puppet::Provider::NameService class DirectoryService < Puppet::Provider::NameService # JJM: Dive into the singleton_class class << self # JJM: This allows us to pass information when calling # Puppet::Type.type # e.g. Puppet::Type.type(:user).provide :directoryservice, :ds_path => "Users" # This is referenced in the get_ds_path class method attr_writer :ds_path attr_writer :macosx_version_major end initvars commands :dscl => "/usr/bin/dscl" commands :dseditgroup => "/usr/sbin/dseditgroup" commands :sw_vers => "/usr/bin/sw_vers" commands :plutil => '/usr/bin/plutil' confine :operatingsystem => :darwin defaultfor :operatingsystem => :darwin # JJM 2007-07-25: This map is used to map NameService attributes to their # corresponding DirectoryService attribute names. # See: http://images.apple.com/server/docs.Open_Directory_v10.4.pdf # JJM: Note, this is de-coupled from the Puppet::Type, and must # be actively maintained. There may also be collisions with different # types (Users, Groups, Mounts, Hosts, etc...) def ds_to_ns_attribute_map; self.class.ds_to_ns_attribute_map; end def self.ds_to_ns_attribute_map { 'RecordName' => :name, 'PrimaryGroupID' => :gid, 'NFSHomeDirectory' => :home, 'UserShell' => :shell, 'UniqueID' => :uid, 'RealName' => :comment, 'Password' => :password, 'GeneratedUID' => :guid, 'IPAddress' => :ip_address, 'ENetAddress' => :en_address, 'GroupMembership' => :members, } end # JJM The same table as above, inverted. def ns_to_ds_attribute_map; self.class.ns_to_ds_attribute_map end def self.ns_to_ds_attribute_map @ns_to_ds_attribute_map ||= ds_to_ns_attribute_map.invert end def self.password_hash_dir '/var/db/shadow/hash' end def self.users_plist_dir '/var/db/dslocal/nodes/Default/users' end def self.instances # JJM Class method that provides an array of instance objects of this # type. # JJM: Properties are dependent on the Puppet::Type we're managine. type_property_array = [:name] + @resource_type.validproperties # Create a new instance of this Puppet::Type for each object present # on the system. list_all_present.collect do |name_string| self.new(single_report(name_string, *type_property_array)) end end def self.get_ds_path # JJM: 2007-07-24 This method dynamically returns the DS path we're concerned with. # For example, if we're working with an user type, this will be /Users # with a group type, this will be /Groups. # @ds_path is an attribute of the class itself. return @ds_path if defined?(@ds_path) # JJM: "Users" or "Groups" etc ... (Based on the Puppet::Type) # Remember this is a class method, so self.class is Class # Also, @resource_type seems to be the reference to the # Puppet::Type this class object is providing for. @resource_type.name.to_s.capitalize + "s" end def self.get_macosx_version_major return @macosx_version_major if defined?(@macosx_version_major) begin # Make sure we've loaded all of the facts Facter.loadfacts if Facter.value(:macosx_productversion_major) product_version_major = Facter.value(:macosx_productversion_major) else # TODO: remove this code chunk once we require Facter 1.5.5 or higher. Puppet.warning("DEPRECATION WARNING: Future versions of the directoryservice provider will require Facter 1.5.5 or newer.") product_version = Facter.value(:macosx_productversion) fail("Could not determine OS X version from Facter") if product_version.nil? product_version_major = product_version.scan(/(\d+)\.(\d+)./).join(".") end fail("#{product_version_major} is not supported by the directoryservice provider") if %w{10.0 10.1 10.2 10.3 10.4}.include?(product_version_major) @macosx_version_major = product_version_major return @macosx_version_major rescue Puppet::ExecutionFailure => detail fail("Could not determine OS X version: #{detail}") end end def self.list_all_present # JJM: List all objects of this Puppet::Type already present on the system. begin dscl_output = execute(get_exec_preamble("-list")) rescue Puppet::ExecutionFailure => detail fail("Could not get #{@resource_type.name} list from DirectoryService") end dscl_output.split("\n") end def self.parse_dscl_plist_data(dscl_output) Plist.parse_xml(dscl_output) end def self.generate_attribute_hash(input_hash, *type_properties) attribute_hash = {} input_hash.keys.each do |key| ds_attribute = key.sub("dsAttrTypeStandard:", "") next unless (ds_to_ns_attribute_map.keys.include?(ds_attribute) and type_properties.include? ds_to_ns_attribute_map[ds_attribute]) ds_value = input_hash[key] case ds_to_ns_attribute_map[ds_attribute] when :members ds_value = ds_value # only members uses arrays so far when :gid, :uid # OS X stores objects like uid/gid as strings. # Try casting to an integer for these cases to be # consistent with the other providers and the group type # validation begin ds_value = Integer(ds_value[0]) rescue ArgumentError ds_value = ds_value[0] end else ds_value = ds_value[0] end attribute_hash[ds_to_ns_attribute_map[ds_attribute]] = ds_value end # NBK: need to read the existing password here as it's not actually # stored in the user record. It is stored at a path that involves the # UUID of the user record for non-Mobile local acccounts. # Mobile Accounts are out of scope for this provider for now attribute_hash[:password] = self.get_password(attribute_hash[:guid], attribute_hash[:name]) if @resource_type.validproperties.include?(:password) and Puppet.features.root? attribute_hash end def self.single_report(resource_name, *type_properties) # JJM 2007-07-24: # Given a the name of an object and a list of properties of that # object, return all property values in a hash. # # This class method returns nil if the object doesn't exist # Otherwise, it returns a hash of the object properties. all_present_str_array = list_all_present # NBK: shortcut the process if the resource is missing return nil unless all_present_str_array.include? resource_name dscl_vector = get_exec_preamble("-read", resource_name) begin dscl_output = execute(dscl_vector) rescue Puppet::ExecutionFailure => detail fail("Could not get report. command execution failed.") end # (#11593) Remove support for OS X 10.4 and earlier fail_if_wrong_version dscl_plist = self.parse_dscl_plist_data(dscl_output) self.generate_attribute_hash(dscl_plist, *type_properties) end def self.fail_if_wrong_version fail("Puppet does not support OS X versions < 10.5") unless self.get_macosx_version_major >= "10.5" end def self.get_exec_preamble(ds_action, resource_name = nil) # JJM 2007-07-24 # DSCL commands are often repetitive and contain the same positional # arguments over and over. See http://developer.apple.com/documentation/Porting/Conceptual/PortingUnix/additionalfeatures/chapter_10_section_9.html # for an example of what I mean. # This method spits out proper DSCL commands for us. # We EXPECT name to be @resource[:name] when called from an instance object. # (#11593) Remove support for OS X 10.4 and earlier fail_if_wrong_version command_vector = [ command(:dscl), "-plist", "." ] # JJM: The actual action to perform. See "man dscl" # Common actiosn: -create, -delete, -merge, -append, -passwd command_vector << ds_action # JJM: get_ds_path will spit back "Users" or "Groups", # etc... Depending on the Puppet::Type of our self. if resource_name command_vector << "/#{get_ds_path}/#{resource_name}" else command_vector << "/#{get_ds_path}" end # JJM: This returns most of the preamble of the command. # e.g. 'dscl / -create /Users/mccune' command_vector end def self.set_password(resource_name, guid, password_hash) # Use Puppet::Util::Package.versioncmp() to catch the scenario where a # version '10.10' would be < '10.7' with simple string comparison. This # if-statement only executes if the current version is less-than 10.7 if (Puppet::Util::Package.versioncmp(get_macosx_version_major, '10.7') == -1) password_hash_file = "#{password_hash_dir}/#{guid}" begin File.open(password_hash_file, 'w') { |f| f.write(password_hash)} rescue Errno::EACCES => detail fail("Could not write to password hash file: #{detail}") end # NBK: For shadow hashes, the user AuthenticationAuthority must contain a value of # ";ShadowHash;". The LKDC in 10.5 makes this more interesting though as it # will dynamically generate ;Kerberosv5;;username@LKDC:SHA1 attributes if # missing. Thus we make sure we only set ;ShadowHash; if it is missing, and # we can do this with the merge command. This allows people to continue to # use other custom AuthenticationAuthority attributes without stomping on them. # # There is a potential problem here in that we're only doing this when setting # the password, and the attribute could get modified at other times while the # hash doesn't change and so this doesn't get called at all... but # without switching all the other attributes to merge instead of create I can't # see a simple enough solution for this that doesn't modify the user record # every single time. This should be a rather rare edge case. (famous last words) dscl_vector = self.get_exec_preamble("-merge", resource_name) dscl_vector << "AuthenticationAuthority" << ";ShadowHash;" begin dscl_output = execute(dscl_vector) rescue Puppet::ExecutionFailure => detail fail("Could not set AuthenticationAuthority.") end else # 10.7 uses salted SHA512 password hashes which are 128 characters plus # an 8 character salt. Previous versions used a SHA1 hash padded with # zeroes. If someone attempts to use a password hash that worked with # a previous version of OX X, we will fail early and warn them. if password_hash.length != 136 fail("OS X 10.7 requires a Salted SHA512 hash password of 136 characters. \ Please check your password and try again.") end if File.exists?("#{users_plist_dir}/#{resource_name}.plist") # If a plist already exists in /var/db/dslocal/nodes/Default/users, then # we will need to extract the binary plist from the 'ShadowHashData' # key, log the new password into the resultant plist's 'SALTED-SHA512' # key, and then save the entire structure back. users_plist = Plist::parse_xml(plutil( '-convert', 'xml1', '-o', '/dev/stdout', \ "#{users_plist_dir}/#{resource_name}.plist")) # users_plist['ShadowHashData'][0].string is actually a binary plist # that's nested INSIDE the user's plist (which itself is a binary - # plist). - password_hash_plist = users_plist['ShadowHashData'][0].string - converted_hash_plist = convert_binary_to_xml(password_hash_plist) + # plist). If we encounter a user plist that DOESN'T have a + # ShadowHashData field, create one. + if users_plist['ShadowHashData'] + password_hash_plist = users_plist['ShadowHashData'][0].string + converted_hash_plist = convert_binary_to_xml(password_hash_plist) + else + users_plist['ShadowHashData'] = [StringIO.new] + converted_hash_plist = {'SALTED-SHA512' => StringIO.new} + end # converted_hash_plist['SALTED-SHA512'].string expects a Base64 encoded # string. The password_hash provided as a resource attribute is a # hex value. We need to convert the provided hex value to a Base64 # encoded string to nest it in the converted hash plist. converted_hash_plist['SALTED-SHA512'].string = \ password_hash.unpack('a2'*(password_hash.size/2)).collect { |i| i.hex.chr }.join # Finally, we can convert the nested plist back to binary, embed it # into the user's plist, and convert the resultant plist back to # a binary plist. changed_plist = convert_xml_to_binary(converted_hash_plist) users_plist['ShadowHashData'][0].string = changed_plist Plist::Emit.save_plist(users_plist, "#{users_plist_dir}/#{resource_name}.plist") plutil('-convert', 'binary1', "#{users_plist_dir}/#{resource_name}.plist") end end end def self.get_password(guid, username) # Use Puppet::Util::Package.versioncmp() to catch the scenario where a # version '10.10' would be < '10.7' with simple string comparison. This - # if-statement only executes if the current version is less-than 10.7 + # if-statement only executes if the current version is less-than 10.7 if (Puppet::Util::Package.versioncmp(get_macosx_version_major, '10.7') == -1) password_hash = nil password_hash_file = "#{password_hash_dir}/#{guid}" if File.exists?(password_hash_file) and File.file?(password_hash_file) fail("Could not read password hash file at #{password_hash_file}") if not File.readable?(password_hash_file) f = File.new(password_hash_file) password_hash = f.read f.close end password_hash else if File.exists?("#{users_plist_dir}/#{username}.plist") # If a plist exists in /var/db/dslocal/nodes/Default/users, we will # extract the binary plist from the 'ShadowHashData' key, decode the # salted-SHA512 password hash, and then return it. users_plist = Plist::parse_xml(plutil('-convert', 'xml1', '-o', '/dev/stdout', "#{users_plist_dir}/#{username}.plist")) if users_plist['ShadowHashData'] # users_plist['ShadowHashData'][0].string is actually a binary plist # that's nested INSIDE the user's plist (which itself is a binary # plist). password_hash_plist = users_plist['ShadowHashData'][0].string converted_hash_plist = convert_binary_to_xml(password_hash_plist) # converted_hash_plist['SALTED-SHA512'].string is a Base64 encoded # string. The password_hash provided as a resource attribute is a # hex value. We need to convert the Base64 encoded string to a # hex value and provide it back to Puppet. password_hash = converted_hash_plist['SALTED-SHA512'].string.unpack("H*")[0] password_hash end end end end # This method will accept a hash that has been returned from Plist::parse_xml # and convert it to a binary plist (string value). def self.convert_xml_to_binary(plist_data) Puppet.debug('Converting XML plist to binary') Puppet.debug('Executing: \'plutil -convert binary1 -o - -\'') IO.popen('plutil -convert binary1 -o - -', mode='r+') do |io| io.write plist_data.to_plist io.close_write @converted_plist = io.read end @converted_plist end # This method will accept a binary plist (as a string) and convert it to a # hash via Plist::parse_xml. def self.convert_binary_to_xml(plist_data) Puppet.debug('Converting binary plist to XML') Puppet.debug('Executing: \'plutil -convert xml1 -o - -\'') IO.popen('plutil -convert xml1 -o - -', mode='r+') do |io| io.write plist_data io.close_write @converted_plist = io.read end Puppet.debug('Converting XML values to a hash.') @plist_hash = Plist::parse_xml(@converted_plist) @plist_hash end # Unlike most other *nixes, OS X doesn't provide built in functionality # for automatically assigning uids and gids to accounts, so we set up these # methods for consumption by functionality like --mkusers # By default we restrict to a reasonably sane range for system accounts def self.next_system_id(id_type, min_id=20) dscl_args = ['.', '-list'] if id_type == 'uid' dscl_args << '/Users' << 'uid' elsif id_type == 'gid' dscl_args << '/Groups' << 'gid' else fail("Invalid id_type #{id_type}. Only 'uid' and 'gid' supported") end dscl_out = dscl(dscl_args) # We're ok with throwing away negative uids here. ids = dscl_out.split.compact.collect { |l| l.to_i if l.match(/^\d+$/) } ids.compact!.sort! { |a,b| a.to_f <=> b.to_f } # We're just looking for an unused id in our sorted array. ids.each_index do |i| next_id = ids[i] + 1 return next_id if ids[i+1] != next_id and next_id >= min_id end end def ensure=(ensure_value) super # We need to loop over all valid properties for the type we're # managing and call the method which sets that property value # dscl can't create everything at once unfortunately. if ensure_value == :present @resource.class.validproperties.each do |name| next if name == :ensure # LAK: We use property.sync here rather than directly calling # the settor method because the properties might do some kind # of conversion. In particular, the user gid property might # have a string and need to convert it to a number if @resource.should(name) @resource.property(name).sync elsif value = autogen(name) self.send(name.to_s + "=", value) else next end end end end def password=(passphrase) exec_arg_vector = self.class.get_exec_preamble("-read", @resource.name) exec_arg_vector << ns_to_ds_attribute_map[:guid] begin guid_output = execute(exec_arg_vector) guid_plist = Plist.parse_xml(guid_output) # Although GeneratedUID like all DirectoryService values can be multi-valued # according to the schema, in practice user accounts cannot have multiple UUIDs # otherwise Bad Things Happen, so we just deal with the first value. guid = guid_plist["dsAttrTypeStandard:#{ns_to_ds_attribute_map[:guid]}"][0] self.class.set_password(@resource.name, guid, passphrase) rescue Puppet::ExecutionFailure => detail fail("Could not set #{param} on #{@resource.class.name}[#{@resource.name}]: #{detail}") end end # NBK: we override @parent.set as we need to execute a series of commands # to deal with array values, rather than the single command nameservice.rb # expects to be returned by modifycmd. Thus we don't bother defining modifycmd. def set(param, value) self.class.validate(param, value) current_members = @property_value_cache_hash[:members] if param == :members # If we are meant to be authoritative for the group membership # then remove all existing members who haven't been specified # in the manifest. remove_unwanted_members(current_members, value) if @resource[:auth_membership] and not current_members.nil? # if they're not a member, make them one. add_members(current_members, value) else exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name]) # JJM: The following line just maps the NS name to the DS name # e.g. { :uid => 'UniqueID' } exec_arg_vector << ns_to_ds_attribute_map[symbolize(param)] # JJM: The following line sends the actual value to set the property to exec_arg_vector << value.to_s begin execute(exec_arg_vector) rescue Puppet::ExecutionFailure => detail fail("Could not set #{param} on #{@resource.class.name}[#{@resource.name}]: #{detail}") end end end # NBK: we override @parent.create as we need to execute a series of commands # to create objects with dscl, rather than the single command nameservice.rb # expects to be returned by addcmd. Thus we don't bother defining addcmd. def create if exists? info "already exists" return nil end # NBK: First we create the object with a known guid so we can set the contents # of the password hash if required # Shelling out sucks, but for a single use case it doesn't seem worth # requiring people install a UUID library that doesn't come with the system. # This should be revisited if Puppet starts managing UUIDs for other platform # user records. guid = %x{/usr/bin/uuidgen}.chomp exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name]) exec_arg_vector << ns_to_ds_attribute_map[:guid] << guid begin execute(exec_arg_vector) rescue Puppet::ExecutionFailure => detail fail("Could not set GeneratedUID for #{@resource.class.name} #{@resource.name}: #{detail}") end if value = @resource.should(:password) and value != "" self.class.set_password(@resource[:name], guid, value) end # Now we create all the standard properties Puppet::Type.type(@resource.class.name).validproperties.each do |property| next if property == :ensure value = @resource.should(property) if property == :gid and value.nil? value = self.class.next_system_id(id_type='gid') end if property == :uid and value.nil? value = self.class.next_system_id(id_type='uid') end if value != "" and not value.nil? if property == :members add_members(nil, value) else exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name]) exec_arg_vector << ns_to_ds_attribute_map[symbolize(property)] next if property == :password # skip setting the password here exec_arg_vector << value.to_s begin execute(exec_arg_vector) rescue Puppet::ExecutionFailure => detail fail("Could not create #{@resource.class.name} #{@resource.name}: #{detail}") end end end end end def remove_unwanted_members(current_members, new_members) current_members.each do |member| if not new_members.flatten.include?(member) cmd = [:dseditgroup, "-o", "edit", "-n", ".", "-d", member, @resource[:name]] begin execute(cmd) rescue Puppet::ExecutionFailure => detail # TODO: We're falling back to removing the member using dscl due to rdar://8481241 # This bug causes dseditgroup to fail to remove a member if that member doesn't exist cmd = [:dscl, ".", "-delete", "/Groups/#{@resource.name}", "GroupMembership", member] begin execute(cmd) rescue Puppet::ExecutionFailure => detail fail("Could not remove #{member} from group: #{@resource.name}, #{detail}") end end end end end def add_members(current_members, new_members) new_members.flatten.each do |new_member| if current_members.nil? or not current_members.include?(new_member) cmd = [:dseditgroup, "-o", "edit", "-n", ".", "-a", new_member, @resource[:name]] begin execute(cmd) rescue Puppet::ExecutionFailure => detail fail("Could not add #{new_member} to group: #{@resource.name}, #{detail}") end end end end def deletecmd # JJM: Like addcmd, only called when deleting the object itself # Note, this isn't used to delete properties of the object, # at least that's how I understand it... self.class.get_exec_preamble("-delete", @resource[:name]) end def getinfo(refresh = false) # JJM 2007-07-24: # Override the getinfo method, which is also defined in nameservice.rb # This method returns and sets @infohash # I'm not re-factoring the name "getinfo" because this method will be # most likely called by nameservice.rb, which I didn't write. if refresh or (! defined?(@property_value_cache_hash) or ! @property_value_cache_hash) # JJM 2007-07-24: OK, there's a bit of magic that's about to # happen... Let's see how strong my grip has become... =) # # self is a provider instance of some Puppet::Type, like # Puppet::Type::User::ProviderDirectoryservice for the case of the # user type and this provider. # # self.class looks like "user provider directoryservice", if that # helps you ... # # self.class.resource_type is a reference to the Puppet::Type class, # probably Puppet::Type::User or Puppet::Type::Group, etc... # # self.class.resource_type.validproperties is a class method, # returning an Array of the valid properties of that specific # Puppet::Type. # # So... something like [:comment, :home, :password, :shell, :uid, # :groups, :ensure, :gid] # # Ultimately, we add :name to the list, delete :ensure from the # list, then report on the remaining list. Pretty whacky, ehh? type_properties = [:name] + self.class.resource_type.validproperties type_properties.delete(:ensure) if type_properties.include? :ensure type_properties << :guid # append GeneratedUID so we just get the report here @property_value_cache_hash = self.class.single_report(@resource[:name], *type_properties) [:uid, :gid].each do |param| @property_value_cache_hash[param] = @property_value_cache_hash[param].to_i if @property_value_cache_hash and @property_value_cache_hash.include?(param) end end @property_value_cache_hash end end end diff --git a/lib/puppet/reports/tagmail.rb b/lib/puppet/reports/tagmail.rb index 7960b2ce4..aeb35b450 100644 --- a/lib/puppet/reports/tagmail.rb +++ b/lib/puppet/reports/tagmail.rb @@ -1,172 +1,179 @@ require 'puppet' require 'pp' require 'net/smtp' require 'time' Puppet::Reports.register_report(:tagmail) do desc "This report sends specific log messages to specific email addresses based on the tags in the log messages. See the [documentation on tags](http://projects.puppetlabs.com/projects/puppet/wiki/Using_Tags) for more information. To use this report, you must create a `tagmail.conf` file in the location specified by the `tagmap` setting. This is a simple file that maps tags to email addresses: Any log messages in the report that match the specified tags will be sent to the specified email addresses. Lines in the `tagmail.conf` file consist of a comma-separated list of tags, a colon, and a comma-separated list of email addresses. Tags can be !negated with a leading exclamation mark, which will subtract any messages with that tag from the set of events handled by that line. Puppet's log levels (`debug`, `info`, `notice`, `warning`, `err`, `alert`, `emerg`, `crit`, and `verbose`) can also be used as tags, and there is an `all` tag that will always match all log messages. An example `tagmail.conf`: all: me@domain.com webserver, !mailserver: httpadmins@domain.com This will send all messages to `me@domain.com`, and all messages from webservers that are not also from mailservers to `httpadmins@domain.com`. If you are using anti-spam controls such as grey-listing on your mail server, you should whitelist the sending email address (controlled by `reportform` configuration option) to ensure your email is not discarded as spam. " # Find all matching messages. def match(taglists) matching_logs = [] taglists.each do |emails, pos, neg| # First find all of the messages matched by our positive tags messages = nil if pos.include?("all") messages = self.logs else # Find all of the messages that are tagged with any of our # tags. messages = self.logs.find_all do |log| pos.detect { |tag| log.tagged?(tag) } end end # Now go through and remove any messages that match our negative tags messages = messages.reject do |log| true if neg.detect do |tag| log.tagged?(tag) end end if messages.empty? Puppet.info "No messages to report to #{emails.join(",")}" next else matching_logs << [emails, messages.collect { |m| m.to_report }.join("\n")] end end matching_logs end # Load the config file def parse(text) taglists = [] text.split("\n").each do |line| taglist = emails = nil case line.chomp when /^\s*#/; next when /^\s*$/; next when /^\s*(.+)\s*:\s*(.+)\s*$/ taglist = $1 emails = $2.sub(/#.*$/,'') else raise ArgumentError, "Invalid tagmail config file" end pos = [] neg = [] taglist.sub(/\s+$/,'').split(/\s*,\s*/).each do |tag| unless tag =~ /^!?[-\w\.]+$/ raise ArgumentError, "Invalid tag #{tag.inspect}" end case tag when /^\w+/; pos << tag when /^!\w+/; neg << tag.sub("!", '') else raise Puppet::Error, "Invalid tag '#{tag}'" end end # Now split the emails emails = emails.sub(/\s+$/,'').split(/\s*,\s*/) taglists << [emails, pos, neg] end taglists end # Process the report. This just calls the other associated messages. def process unless FileTest.exists?(Puppet[:tagmap]) Puppet.notice "Cannot send tagmail report; no tagmap file #{Puppet[:tagmap]}" return end + metrics = raw_summary['resources'] || {} rescue {} + + if metrics['out_of_sync'] == 0 && metrics['changed'] == 0 + Puppet.notice "Not sending tagmail report; no changes" + return + end + taglists = parse(File.read(Puppet[:tagmap])) # Now find any appropriately tagged messages. reports = match(taglists) send(reports) end # Send the email reports. def send(reports) pid = fork do if Puppet[:smtpserver] != "none" begin Net::SMTP.start(Puppet[:smtpserver]) do |smtp| reports.each do |emails, messages| smtp.open_message_stream(Puppet[:reportfrom], *emails) do |p| p.puts "From: #{Puppet[:reportfrom]}" p.puts "Subject: Puppet Report for #{self.host}" p.puts "To: " + emails.join(", ") p.puts "Date: #{Time.now.rfc2822}" p.puts p.puts messages end end end rescue => detail message = "Could not send report emails through smtp: #{detail}" Puppet.log_exception(detail, message) raise Puppet::Error, message end elsif Puppet[:sendmail] != "" begin reports.each do |emails, messages| # We need to open a separate process for every set of email addresses IO.popen(Puppet[:sendmail] + " " + emails.join(" "), "w") do |p| p.puts "From: #{Puppet[:reportfrom]}" p.puts "Subject: Puppet Report for #{self.host}" p.puts "To: " + emails.join(", ") p.puts messages end end rescue => detail message = "Could not send report emails via sendmail: #{detail}" Puppet.log_exception(detail, message) raise Puppet::Error, message end else raise Puppet::Error, "SMTP server is unset and could not find sendmail" end end # Don't bother waiting for the pid to return. Process.detach(pid) end end diff --git a/lib/puppet/type/user.rb b/lib/puppet/type/user.rb index 6556a249a..f3496a8fa 100755 --- a/lib/puppet/type/user.rb +++ b/lib/puppet/type/user.rb @@ -1,523 +1,524 @@ require 'etc' require 'facter' require 'puppet/property/list' require 'puppet/property/ordered_list' require 'puppet/property/keyvalue' module Puppet newtype(:user) do @doc = "Manage users. This type is mostly built to manage system users, so it is lacking some features useful for managing normal users. This resource type uses the prescribed native tools for creating groups and generally uses POSIX APIs for retrieving information about them. It does not directly modify `/etc/passwd` or anything. **Autorequires:** If Puppet is managing the user's primary group (as provided in the `gid` attribute), the user resource will autorequire that group. If Puppet is managing any role accounts corresponding to the user's roles, the user resource will autorequire those role accounts." feature :allows_duplicates, "The provider supports duplicate users with the same UID." feature :manages_homedir, "The provider can create and remove home directories." feature :manages_passwords, "The provider can modify user passwords, by accepting a password hash." feature :manages_password_age, "The provider can set age requirements and restrictions for passwords." feature :manages_solaris_rbac, "The provider can manage roles and normal users" feature :manages_expiry, "The provider can manage the expiry date for a user." feature :system_users, "The provider allows you to create system users with lower UIDs." feature :manages_aix_lam, "The provider can manage AIX Loadable Authentication Module (LAM) system." newproperty(:ensure, :parent => Puppet::Property::Ensure) do newvalue(:present, :event => :user_created) do provider.create end newvalue(:absent, :event => :user_removed) do provider.delete end newvalue(:role, :event => :role_created, :required_features => :manages_solaris_rbac) do provider.create_role end desc "The basic state that the object should be in." # If they're talking about the thing at all, they generally want to # say it should exist. defaultto do if @resource.managed? :present else nil end end def retrieve if provider.exists? if provider.respond_to?(:is_role?) and provider.is_role? return :role else return :present end else return :absent end end end newproperty(:home) do desc "The home directory of the user. The directory must be created separately and is not currently checked for existence." end newproperty(:uid) do desc "The user ID; must be specified numerically. If no user ID is specified when creating a new user, then one will be chosen automatically. This will likely result in the same user having different UIDs on different systems, which is not recommended. This is especially noteworthy when managing the same user on both Darwin and other platforms, since Puppet does UID generation on Darwin, but the underlying tools do so on other platforms. On Windows, this property is read-only and will return the user's security identifier (SID)." munge do |value| case value when String if value =~ /^[-0-9]+$/ value = Integer(value) end end return value end end newproperty(:gid) do desc "The user's primary group. Can be specified numerically or by name. Note that users on Windows systems do not have a primary group; manage groups with the `groups` attribute instead." munge do |value| if value.is_a?(String) and value =~ /^[-0-9]+$/ Integer(value) else value end end def insync?(is) # We know the 'is' is a number, so we need to convert the 'should' to a number, # too. @should.each do |value| return true if number = Puppet::Util.gid(value) and is == number end false end def sync found = false @should.each do |value| if number = Puppet::Util.gid(value) provider.gid = number found = true break end end fail "Could not find group(s) #{@should.join(",")}" unless found # Use the default event. end end newproperty(:comment) do desc "A description of the user. Generally the user's full name." end newproperty(:shell) do desc "The user's login shell. The shell must exist and be executable. This attribute cannot be managed on Windows systems." end newproperty(:password, :required_features => :manages_passwords) do desc %q{The user's password, in whatever encrypted format the local system requires. * Most modern Unix-like systems use salted SHA1 password hashes. You can use Puppet's built-in `sha1` function to generate a hash from a password. * Mac OS X 10.5 and 10.6 also use salted SHA1 hashes. * Mac OS X 10.7 (Lion) uses salted SHA512 hashes. The Puppet Labs [stdlib][] module contains a `str2saltedsha512` function which can generate password hashes for Lion. * Windows passwords can only be managed in cleartext, as there is no Windows API for setting the password hash. [stdlib]: https://github.com/puppetlabs/puppetlabs-stdlib/ Be sure to enclose any value that includes a dollar sign ($) in single quotes (') to avoid accidental variable interpolation.} validate do |value| raise ArgumentError, "Passwords cannot include ':'" if value.is_a?(String) and value.include?(":") end def change_to_s(currentvalue, newvalue) if currentvalue == :absent return "created password" else return "changed password" end end def is_to_s( currentvalue ) return '[old password hash redacted]' end def should_to_s( newvalue ) return '[new password hash redacted]' end end newproperty(:password_min_age, :required_features => :manages_password_age) do desc "The minimum number of days a password must be used before it may be changed." munge do |value| case value when String Integer(value) else value end end validate do |value| if value.to_s !~ /^-?\d+$/ raise ArgumentError, "Password minimum age must be provided as a number." end end end newproperty(:password_max_age, :required_features => :manages_password_age) do desc "The maximum number of days a password may be used before it must be changed." munge do |value| case value when String Integer(value) else value end end validate do |value| if value.to_s !~ /^-?\d+$/ raise ArgumentError, "Password maximum age must be provided as a number." end end end newproperty(:groups, :parent => Puppet::Property::List) do desc "The groups to which the user belongs. The primary group should not be listed, and groups should be identified by name rather than by GID. Multiple groups should be specified as an array." validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Group names must be provided, not GID numbers." end raise ArgumentError, "Group names must be provided as an array, not a comma-separated list." if value.include?(",") + raise ArgumentError, "Group names must not be empty. If you want to specify \"no groups\" pass an empty array" if value.empty? end end newparam(:name) do desc "The user name. While naming limitations vary by operating system, it is advisable to restrict names to the lowest common denominator, which is a maximum of 8 characters beginning with a letter. Note that Puppet considers user names to be case-sensitive, regardless of the platform's own rules; be sure to always use the same case when referring to a given user." isnamevar end newparam(:membership) do desc "Whether specified groups should be considered the **complete list** (`inclusive`) or the **minimum list** (`minimum`) of groups to which the user belongs. Defaults to `minimum`." newvalues(:inclusive, :minimum) defaultto :minimum end newparam(:system, :boolean => true) do desc "Whether the user is a system user, according to the OS's criteria; on most platforms, a UID less than or equal to 500 indicates a system user. Defaults to `false`." newvalues(:true, :false) defaultto false end newparam(:allowdupe, :boolean => true) do desc "Whether to allow duplicate UIDs. Defaults to `false`." newvalues(:true, :false) defaultto false end newparam(:managehome, :boolean => true) do desc "Whether to manage the home directory when managing the user. Defaults to `false`." newvalues(:true, :false) defaultto false validate do |val| if val.to_s == "true" raise ArgumentError, "User provider #{provider.class.name} can not manage home directories" unless provider.class.manages_homedir? end end end newproperty(:expiry, :required_features => :manages_expiry) do desc "The expiry date for this user. Must be provided in a zero-padded YYYY-MM-DD format --- e.g. 2010-02-19." validate do |value| if value !~ /^\d{4}-\d{2}-\d{2}$/ raise ArgumentError, "Expiry dates must be YYYY-MM-DD" end end end # Autorequire the group, if it's around autorequire(:group) do autos = [] if obj = @parameters[:gid] and groups = obj.shouldorig groups = groups.collect { |group| if group =~ /^\d+$/ Integer(group) else group end } groups.each { |group| case group when Integer if resource = catalog.resources.find { |r| r.is_a?(Puppet::Type.type(:group)) and r.should(:gid) == group } autos << resource end else autos << group end } end if obj = @parameters[:groups] and groups = obj.should autos += groups.split(",") end autos end # Provide an external hook. Yay breaking out of APIs. def exists? provider.exists? end def retrieve absent = false properties.inject({}) { |prophash, property| current_value = :absent if absent prophash[property] = :absent else current_value = property.retrieve prophash[property] = current_value end if property.name == :ensure and current_value == :absent absent = true end prophash } end newproperty(:roles, :parent => Puppet::Property::List, :required_features => :manages_solaris_rbac) do desc "The roles the user has. Multiple roles should be specified as an array." def membership :role_membership end validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Role names must be provided, not numbers" end raise ArgumentError, "Role names must be provided as an array, not a comma-separated list" if value.include?(",") end end #autorequire the roles that the user has autorequire(:user) do reqs = [] if roles_property = @parameters[:roles] and roles = roles_property.should reqs += roles.split(',') end reqs end newparam(:role_membership) do desc "Whether specified roles should be considered the **complete list** (`inclusive`) or the **minimum list** (`minimum`) of roles the user has. Defaults to `minimum`." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:auths, :parent => Puppet::Property::List, :required_features => :manages_solaris_rbac) do desc "The auths the user has. Multiple auths should be specified as an array." def membership :auth_membership end validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Auth names must be provided, not numbers" end raise ArgumentError, "Auth names must be provided as an array, not a comma-separated list" if value.include?(",") end end newparam(:auth_membership) do desc "Whether specified auths should be considered the **complete list** (`inclusive`) or the **minimum list** (`minimum`) of auths the user has. Defaults to `minimum`." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:profiles, :parent => Puppet::Property::OrderedList, :required_features => :manages_solaris_rbac) do desc "The profiles the user has. Multiple profiles should be specified as an array." def membership :profile_membership end validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Profile names must be provided, not numbers" end raise ArgumentError, "Profile names must be provided as an array, not a comma-separated list" if value.include?(",") end end newparam(:profile_membership) do desc "Whether specified roles should be treated as the **complete list** (`inclusive`) or the **minimum list** (`minimum`) of roles of which the user is a member. Defaults to `minimum`." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:keys, :parent => Puppet::Property::KeyValue, :required_features => :manages_solaris_rbac) do desc "Specify user attributes in an array of key = value pairs." def membership :key_membership end validate do |value| raise ArgumentError, "Key/value pairs must be separated by an =" unless value.include?("=") end end newparam(:key_membership) do desc "Whether specified key/value pairs should be considered the **complete list** (`inclusive`) or the **minimum list** (`minimum`) of the user's attributes. Defaults to `minimum`." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:project, :required_features => :manages_solaris_rbac) do desc "The name of the project associated with a user." end newparam(:ia_load_module, :required_features => :manages_aix_lam) do desc "The name of the I&A module to use to manage this user." end newproperty(:attributes, :parent => Puppet::Property::KeyValue, :required_features => :manages_aix_lam) do desc "Specify AIX attributes for the user in an array of attribute = value pairs." def membership :attribute_membership end def delimiter " " end validate do |value| raise ArgumentError, "Attributes value pairs must be separated by an =" unless value.include?("=") end end newparam(:attribute_membership) do desc "Whether specified attribute value pairs should be treated as the **complete list** (`inclusive`) or the **minimum list** (`minimum`) of attribute/value pairs for the user. Defaults to `minimum`." newvalues(:inclusive, :minimum) defaultto :minimum end end end diff --git a/lib/puppet/util.rb b/lib/puppet/util.rb index 76bfbe6fd..bb83e0999 100644 --- a/lib/puppet/util.rb +++ b/lib/puppet/util.rb @@ -1,529 +1,529 @@ # A module to collect utility functions. require 'English' require 'puppet/external/lock' require 'puppet/error' require 'puppet/util/execution_stub' require 'uri' require 'sync' require 'monitor' require 'tempfile' require 'pathname' module Puppet module Util require 'puppet/util/monkey_patches' require 'benchmark' # These are all for backward compatibility -- these are methods that used # to be in Puppet::Util but have been moved into external modules. require 'puppet/util/posix' extend Puppet::Util::POSIX @@sync_objects = {}.extend MonitorMixin def self.activerecord_version if (defined?(::ActiveRecord) and defined?(::ActiveRecord::VERSION) and defined?(::ActiveRecord::VERSION::MAJOR) and defined?(::ActiveRecord::VERSION::MINOR)) ([::ActiveRecord::VERSION::MAJOR, ::ActiveRecord::VERSION::MINOR].join('.').to_f) else 0 end end # Run some code with a specific environment. Resets the environment back to # what it was at the end of the code. def self.withenv(hash) saved = ENV.to_hash hash.each do |name, val| ENV[name.to_s] = val end yield ensure ENV.clear saved.each do |name, val| ENV[name] = val end end # Execute a given chunk of code with a new umask. def self.withumask(mask) cur = File.umask(mask) begin yield ensure File.umask(cur) end end def self.synchronize_on(x,type) sync_object,users = 0,1 begin @@sync_objects.synchronize { (@@sync_objects[x] ||= [Sync.new,0])[users] += 1 } @@sync_objects[x][sync_object].synchronize(type) { yield } ensure @@sync_objects.synchronize { @@sync_objects.delete(x) unless (@@sync_objects[x][users] -= 1) > 0 } end end # Change the process to a different user def self.chuser if group = Puppet[:group] begin Puppet::Util::SUIDManager.change_group(group, true) rescue => detail Puppet.warning "could not change to group #{group.inspect}: #{detail}" $stderr.puts "could not change to group #{group.inspect}" # Don't exit on failed group changes, since it's # not fatal #exit(74) end end if user = Puppet[:user] begin Puppet::Util::SUIDManager.change_user(user, true) rescue => detail $stderr.puts "Could not change to user #{user}: #{detail}" exit(74) end end end # Create instance methods for each of the log levels. This allows # the messages to be a little richer. Most classes will be calling this # method. def self.logmethods(klass, useself = true) Puppet::Util::Log.eachlevel { |level| klass.send(:define_method, level, proc { |args| args = args.join(" ") if args.is_a?(Array) if useself Puppet::Util::Log.create( :level => level, :source => self, :message => args ) else Puppet::Util::Log.create( :level => level, :message => args ) end }) } end # Proxy a bunch of methods to another object. def self.classproxy(klass, objmethod, *methods) classobj = class << klass; self; end methods.each do |method| classobj.send(:define_method, method) do |*args| obj = self.send(objmethod) obj.send(method, *args) end end end # Proxy a bunch of methods to another object. def self.proxy(klass, objmethod, *methods) methods.each do |method| klass.send(:define_method, method) do |*args| obj = self.send(objmethod) obj.send(method, *args) end end end def benchmark(*args) msg = args.pop level = args.pop object = nil if args.empty? if respond_to?(level) object = self else object = Puppet end else object = args.pop end raise Puppet::DevError, "Failed to provide level to :benchmark" unless level unless level == :none or object.respond_to? level raise Puppet::DevError, "Benchmarked object does not respond to #{level}" end # Only benchmark if our log level is high enough if level != :none and Puppet::Util::Log.sendlevel?(level) result = nil seconds = Benchmark.realtime { yield } object.send(level, msg + (" in %0.2f seconds" % seconds)) return seconds else yield end end def which(bin) if absolute_path?(bin) return bin if FileTest.file? bin and FileTest.executable? bin else ENV['PATH'].split(File::PATH_SEPARATOR).each do |dir| begin dest = File.expand_path(File.join(dir, bin)) rescue ArgumentError => e # if the user's PATH contains a literal tilde (~) character and HOME is not set, we may get # an ArgumentError here. Let's check to see if that is the case; if not, re-raise whatever error # was thrown. raise e unless ((dir =~ /~/) && ((ENV['HOME'].nil? || ENV['HOME'] == ""))) # if we get here they have a tilde in their PATH. We'll issue a single warning about this and then # ignore this path element and carry on with our lives. Puppet::Util::Warnings.warnonce("PATH contains a ~ character, and HOME is not set; ignoring PATH element '#{dir}'.") next end if Puppet.features.microsoft_windows? && File.extname(dest).empty? exts = ENV['PATHEXT'] exts = exts ? exts.split(File::PATH_SEPARATOR) : %w[.COM .EXE .BAT .CMD] exts.each do |ext| destext = File.expand_path(dest + ext) return destext if FileTest.file? destext and FileTest.executable? destext end end return dest if FileTest.file? dest and FileTest.executable? dest end end nil end module_function :which # Determine in a platform-specific way whether a path is absolute. This # defaults to the local platform if none is specified. def absolute_path?(path, platform=nil) # Escape once for the string literal, and once for the regex. slash = '[\\\\/]' name = '[^\\\\/]+' regexes = { :windows => %r!^(([A-Z]:#{slash})|(#{slash}#{slash}#{name}#{slash}#{name})|(#{slash}#{slash}\?#{slash}#{name}))!i, :posix => %r!^/!, } require 'puppet' platform ||= Puppet.features.microsoft_windows? ? :windows : :posix !! (path =~ regexes[platform]) end module_function :absolute_path? # Convert a path to a file URI def path_to_uri(path) return unless path params = { :scheme => 'file' } if Puppet.features.microsoft_windows? path = path.gsub(/\\/, '/') if unc = /^\/\/([^\/]+)(\/[^\/]+)/.match(path) params[:host] = unc[1] path = unc[2] elsif path =~ /^[a-z]:\//i path = '/' + path end end params[:path] = URI.escape(path) begin URI::Generic.build(params) rescue => detail raise Puppet::Error, "Failed to convert '#{path}' to URI: #{detail}" end end module_function :path_to_uri # Get the path component of a URI def uri_to_path(uri) return unless uri.is_a?(URI) path = URI.unescape(uri.path) if Puppet.features.microsoft_windows? and uri.scheme == 'file' if uri.host path = "//#{uri.host}" + path # UNC else path.sub!(/^\//, '') end end path end module_function :uri_to_path # Create an exclusive lock. def threadlock(resource, type = Sync::EX) Puppet::Util.synchronize_on(resource,type) { yield } end module_function :benchmark def memory unless defined?(@pmap) @pmap = which('pmap') end if @pmap %x{#{@pmap} #{Process.pid}| grep total}.chomp.sub(/^\s*total\s+/, '').sub(/K$/, '').to_i else 0 end end def symbolize(value) if value.respond_to? :intern value.intern else value end end def symbolizehash(hash) newhash = {} hash.each do |name, val| if name.is_a? String newhash[name.intern] = val else newhash[name] = val end end + newhash end def symbolizehash!(hash) - hash.each do |name, val| - if name.is_a? String - hash[name.intern] = val - hash.delete(name) - end - end + # this is not the most memory-friendly way to accomplish this, but the + # code re-use and clarity seems worthwhile. + newhash = symbolizehash(hash) + hash.clear + hash.merge!(newhash) hash end module_function :symbolize, :symbolizehash, :symbolizehash! # Just benchmark, with no logging. def thinmark seconds = Benchmark.realtime { yield } seconds end module_function :memory, :thinmark # Because IO#binread is only available in 1.9 def binread(file) File.open(file, 'rb') { |f| f.read } end module_function :binread # utility method to get the current call stack and format it to a human-readable string (which some IDEs/editors # will recognize as links to the line numbers in the trace) def self.pretty_backtrace(backtrace = caller(1)) backtrace.collect do |line| file_path, line_num = line.split(":") file_path = expand_symlinks(File.expand_path(file_path)) file_path + ":" + line_num end .join("\n") end # utility method that takes a path as input, checks each component of the path to see if it is a symlink, and expands # it if it is. returns the expanded path. def self.expand_symlinks(file_path) file_path.split("/").inject do |full_path, next_dir| next_path = full_path + "/" + next_dir if File.symlink?(next_path) then link = File.readlink(next_path) next_path = case link when /^\// then link else File.expand_path(full_path + "/" + link) end end next_path end end # Replace a file, securely. This takes a block, and passes it the file # handle of a file open for writing. Write the replacement content inside # the block and it will safely replace the target file. # # This method will make no changes to the target file until the content is # successfully written and the block returns without raising an error. # # As far as possible the state of the existing file, such as mode, is # preserved. This works hard to avoid loss of any metadata, but will result # in an inode change for the file. # # Arguments: `filename`, `default_mode` # # The filename is the file we are going to replace. # # The default_mode is the mode to use when the target file doesn't already # exist; if the file is present we copy the existing mode/owner/group values # across. def replace_file(file, default_mode, &block) raise Puppet::DevError, "replace_file requires a block" unless block_given? file = Pathname(file) tempfile = Tempfile.new(file.basename.to_s, file.dirname.to_s) file_exists = file.exist? # If the file exists, use its current mode/owner/group. If it doesn't, use # the supplied mode, and default to current user/group. if file_exists if Puppet.features.microsoft_windows? mode = Puppet::Util::Windows::Security.get_mode(file.to_s) uid = Puppet::Util::Windows::Security.get_owner(file.to_s) gid = Puppet::Util::Windows::Security.get_owner(file.to_s) else stat = file.lstat mode = stat.mode uid = stat.uid gid = stat.gid end # We only care about the four lowest-order octets. Higher octets are # filesystem-specific. mode &= 07777 else mode = default_mode uid = Process.euid gid = Process.egid end # Set properties of the temporary file before we write the content, because # Tempfile doesn't promise to be safe from reading by other people, just # that it avoids races around creating the file. if Puppet.features.microsoft_windows? Puppet::Util::Windows::Security.set_mode(mode, tempfile.path) Puppet::Util::Windows::Security.set_owner(uid, tempfile.path) Puppet::Util::Windows::Security.set_group(gid, tempfile.path) else tempfile.chmod(mode) tempfile.chown(uid, gid) end # OK, now allow the caller to write the content of the file. yield tempfile # Now, make sure the data (which includes the mode) is safe on disk. tempfile.flush begin tempfile.fsync rescue NotImplementedError # fsync may not be implemented by Ruby on all platforms, but # there is absolutely no recovery path if we detect that. So, we just # ignore the return code. # # However, don't be fooled: that is accepting that we are running in # an unsafe fashion. If you are porting to a new platform don't stub # that out. end tempfile.close File.rename(tempfile.path, file) # Ideally, we would now fsync the directory as well, but Ruby doesn't # have support for that, and it doesn't matter /that/ much... # Return something true, and possibly useful. file end module_function :replace_file # Executes a block of code, wrapped with some special exception handling. Causes the ruby interpreter to # exit if the block throws an exception. # # @param [String] message a message to log if the block fails # @param [Integer] code the exit code that the ruby interpreter should return if the block fails # @yield def exit_on_fail(message, code = 1) yield # First, we need to check and see if we are catching a SystemExit error. These will be raised # when we daemonize/fork, and they do not necessarily indicate a failure case. rescue SystemExit => err raise err # Now we need to catch *any* other kind of exception, because we may be calling third-party # code (e.g. webrick), and we have no idea what they might throw. rescue Exception => err Puppet.log_exception(err, "Could not #{message}: #{err}") exit(code) end module_function :exit_on_fail ####################################################################################################### # Deprecated methods relating to process execution; these have been moved to Puppet::Util::Execution ####################################################################################################### def execpipe(command, failonfail = true, &block) Puppet.deprecation_warning("Puppet::Util.execpipe is deprecated; please use Puppet::Util::Execution.execpipe") Puppet::Util::Execution.execpipe(command, failonfail, &block) end module_function :execpipe def execfail(command, exception) Puppet.deprecation_warning("Puppet::Util.execfail is deprecated; please use Puppet::Util::Execution.execfail") Puppet::Util::Execution.execfail(command, exception) end module_function :execfail def execute(command, arguments = {}) Puppet.deprecation_warning("Puppet::Util.execute is deprecated; please use Puppet::Util::Execution.execute") Puppet::Util::Execution.execute(command, arguments) end module_function :execute end end require 'puppet/util/errors' require 'puppet/util/methodhelper' require 'puppet/util/metaid' require 'puppet/util/classgen' require 'puppet/util/docs' require 'puppet/util/execution' require 'puppet/util/logging' require 'puppet/util/package' require 'puppet/util/warnings' diff --git a/lib/puppet/util/monkey_patches.rb b/lib/puppet/util/monkey_patches.rb index 952aa2790..87ffc617e 100644 --- a/lib/puppet/util/monkey_patches.rb +++ b/lib/puppet/util/monkey_patches.rb @@ -1,230 +1,275 @@ require 'puppet/util' module Puppet::Util::MonkeyPatches end 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 unless method_defined? "<=>" 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 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 # Defined in 1.9, absent in 1.8, and used for compatibility in various # places, typically in third party gems. def intern return self end unless method_defined? :intern end class String unless method_defined? :lines require 'puppet/util/monkey_patches/lines' include Puppet::Util::MonkeyPatches::Lines end end require 'fcntl' class IO unless method_defined? :lines require 'puppet/util/monkey_patches/lines' include Puppet::Util::MonkeyPatches::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 = nil) # Determine if we should truncate or not. Since the truncate method on a # file handle isn't implemented on all platforms, safer to do this in what # looks like the libc interface - which is usually pretty robust. # --daniel 2012-03-11 mode = Fcntl::O_CREAT | Fcntl::O_WRONLY | (offset.nil? ? Fcntl::O_TRUNC : 0) IO.open(IO::sysopen(name, mode)) do |f| # ...seek to our desired offset, then write the bytes. Don't try to # seek past the start of the file, eh, because who knows what platform # would legitimately blow up if we did that. # # Double-check the positioning, too, since destroying data isn't my idea # of a good time. --daniel 2012-03-11 target = [0, offset.to_i].max unless (landed = f.sysseek(target, IO::SEEK_SET)) == target raise "unable to seek to target offset #{target} in #{name}: got to #{landed}" end f.syswrite(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 return type of `instance_variables` changes between Ruby 1.8 and 1.9 # releases; it used to return an array of strings in the form "@foo", but # now returns an array of symbols in the form :@foo. # # Nothing else in the stack cares which form you get - you can pass the # string or symbol to things like `instance_variable_set` and they will work # transparently. # # Having the same form in all releases of Puppet is a win, though, so we # pick a unification and enforce than on all releases. That way developers # who do set math on them (eg: for YAML rendering) don't have to handle the # distinction themselves. # # In the sane tradition, we bring older releases into conformance with newer # releases, so we return symbols rather than strings, to be more like the # future versions of Ruby are. # # We also carefully support reloading, by only wrapping when we don't # already have the original version of the method aliased away somewhere. if RUBY_VERSION[0,3] == '1.8' unless Object.respond_to?(:puppet_original_instance_variables) # Add our wrapper to the method. class Object alias :puppet_original_instance_variables :instance_variables def instance_variables puppet_original_instance_variables.map(&:to_sym) end end # The one place that Ruby 1.8 assumes something about the return format of # the `instance_variables` method is actually kind of odd, because it uses # eval to get at instance variables of another object. # # This takes the original code and applies replaces instance_eval with # instance_variable_get through it. All other bugs in the original (such # as equality depending on the instance variables having the same order # without any promise from the runtime) are preserved. --daniel 2012-03-11 require 'resolv' class Resolv::DNS::Resource def ==(other) # :nodoc: return self.class == other.class && self.instance_variables == other.instance_variables && self.instance_variables.collect {|name| self.instance_variable_get name} == other.instance_variables.collect {|name| other.instance_variable_get name} end end end 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/fixtures/unit/reports/tagmail/tagmail_email.conf b/spec/fixtures/unit/reports/tagmail/tagmail_email.conf new file mode 100644 index 000000000..94efca400 --- /dev/null +++ b/spec/fixtures/unit/reports/tagmail/tagmail_email.conf @@ -0,0 +1,2 @@ +secure: user@domain.com + diff --git a/spec/integration/faces/documentation_spec.rb b/spec/integration/faces/documentation_spec.rb index 9ddf2f1b3..371143c6a 100755 --- a/spec/integration/faces/documentation_spec.rb +++ b/spec/integration/faces/documentation_spec.rb @@ -1,55 +1,55 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/face' describe "documentation of faces" do it "should generate global help" do help = nil expect { help = Puppet::Face[:help, :current].help }.not_to raise_error help.should be_an_instance_of String help.length.should be > 200 end ######################################################################## # Can we actually generate documentation for the face, and the actions it # has? This avoids situations where the ERB template turns out to have a # bug in it, triggered in something the user might do. Puppet::Face.faces.sort.each do |face_name| # REVISIT: We should walk all versions of the face here... let :help do Puppet::Face[:help, :current] end context "generating help" do it "for #{face_name}" do expect { text = help.help(face_name) text.should be_an_instance_of String text.length.should be > 100 }.not_to raise_error end Puppet::Face[face_name, :current].actions.sort.each do |action_name| it "for #{face_name}.#{action_name}" do expect { text = help.help(face_name, action_name) text.should be_an_instance_of String text.length.should be > 100 }.not_to raise_error end end end ######################################################################## # Ensure that we have authorship and copyright information in *our* faces; # if you apply this to third party faces you might well be disappointed. context "licensing of Puppet Labs face '#{face_name}'" do subject { Puppet::Face[face_name, :current] } its :license do should =~ /Apache\s*2/ end its :copyright do should =~ /Puppet Labs/ end # REVISIT: This is less that ideal, I think, but right now I am more # comfortable watching us ship with some copyright than without any; we # can redress that when it becomes appropriate. --daniel 2011-04-27 - its :copyright do should =~ /2011/ end + its :copyright do should =~ /20\d{2}/ end end end end diff --git a/spec/integration/module_tool_spec.rb b/spec/integration/module_tool_spec.rb deleted file mode 100644 index bd3b12649..000000000 --- a/spec/integration/module_tool_spec.rb +++ /dev/null @@ -1,475 +0,0 @@ -require 'spec_helper' -require 'tmpdir' -require 'fileutils' - -# FIXME This are helper methods that could be used by other tests in the -# future, should we move these to a more central location -def stub_repository_read(code, body) - kind = Net::HTTPResponse.send(:response_class, code.to_s) - response = kind.new('1.0', code.to_s, 'HTTP MESSAGE') - response.stubs(:read_body).returns(body) - Puppet::Forge::Repository.any_instance.stubs(:read_response).returns(response) -end - -def stub_installer_read(body) - Puppet::Module::Tool::Applications::Installer.any_instance.stubs(:read_match).returns(body) -end - -def stub_cache_read(body) - Puppet::Forge::Cache.any_instance.stubs(:read_retrieve).returns(body) -end - -# Return path to temparory directory for testing. -def testdir - return @testdir ||= tmpdir("module_tool_testdir") -end - -# Create a temporary testing directory, change into it, and execute the -# +block+. When the block exists, remove the test directory and change back -# to the previous directory. -def mktestdircd(&block) - previousdir = Dir.pwd - rmtestdir - FileUtils.mkdir_p(testdir) - Dir.chdir(testdir) - block.call -ensure - rmtestdir - Dir.chdir previousdir -end - -# Remove the temporary test directory. -def rmtestdir - FileUtils.rm_rf(testdir) if File.directory?(testdir) -end -# END helper methods - - -# Directory that contains sample releases. -RELEASE_FIXTURES_DIR = File.join(PuppetSpec::FIXTURE_DIR, "releases") - -# Return the pathname string to the directory containing the release fixture called +name+. -def release_fixture(name) - return File.join(RELEASE_FIXTURES_DIR, name) -end - -# Copy the release fixture called +name+ into the current working directory. -def install_release_fixture(name) - release_fixture(name) - FileUtils.cp_r(release_fixture(name), name) -end - -describe "module_tool", :fails_on_windows => true do - include PuppetSpec::Files - before do - @tmp_confdir = Puppet[:confdir] = tmpdir("module_tool_test_confdir") - @tmp_vardir = Puppet[:vardir] = tmpdir("module_tool_test_vardir") - Puppet[:module_repository] = "http://forge.puppetlabs.com" - @mytmpdir = Pathname.new(tmpdir("module_tool_test")) - @options = {} - @options[:install_dir] = @mytmpdir - @options[:module_repository] = "http://forge.puppetlabs.com" - end - - def build_and_install_module - Puppet::Module::Tool::Applications::Generator.run(@full_module_name) - Puppet::Module::Tool::Applications::Builder.run(@full_module_name) - - FileUtils.mv("#{@full_module_name}/pkg/#{@release_name}.tar.gz", "#{@release_name}.tar.gz") - FileUtils.rm_rf(@full_module_name) - - Puppet::Module::Tool::Applications::Installer.run("#{@release_name}.tar.gz", @options) - end - - # Return STDOUT and STDERR output generated from +block+ as it's run within a temporary test directory. - def run(&block) - mktestdircd do - block.call - end - end - - before :all do - @username = "myuser" - @module_name = "mymodule" - @full_module_name = "#{@username}-#{@module_name}" - @version = "0.0.1" - @release_name = "#{@full_module_name}-#{@version}" - end - - before :each do - Puppet.settings.stubs(:parse) - Puppet::Forge::Cache.clean - end - - after :each do - Puppet::Forge::Cache.clean - end - - describe "generate" do - it "should generate a module if given a dashed name" do - run do - Puppet::Module::Tool::Applications::Generator.run(@full_module_name) - - File.directory?(@full_module_name).should == true - modulefile = File.join(@full_module_name, "Modulefile") - File.file?(modulefile).should == true - metadata = Puppet::Module::Tool::Metadata.new - Puppet::Module::Tool::ModulefileReader.evaluate(metadata, modulefile) - metadata.full_module_name.should == @full_module_name - metadata.username.should == @username - metadata.name.should == @module_name - end - end - - it "should fail if given an undashed name" do - run do - lambda { Puppet::Module::Tool::Applications::Generator.run("invalid") }.should raise_error(RuntimeError) - end - end - - it "should fail if directory already exists" do - run do - Puppet::Module::Tool::Applications::Generator.run(@full_module_name) - lambda { Puppet::Module::Tool::Applications::Generator.run(@full_module_name) }.should raise_error(ArgumentError) - end - end - - it "should return an array of Pathname objects representing paths of generated files" do - run do - return_value = Puppet::Module::Tool::Applications::Generator.run(@full_module_name) - return_value.each do |generated_file| - generated_file.should be_kind_of(Pathname) - end - return_value.should be_kind_of(Array) - end - end - end - - describe "build" do - it "should build a module in a directory" do - run do - Puppet::Module::Tool::Applications::Generator.run(@full_module_name) - Puppet::Module::Tool::Applications::Builder.run(@full_module_name) - - File.directory?(File.join(@full_module_name, "pkg", @release_name)).should == true - File.file?(File.join(@full_module_name, "pkg", @release_name + ".tar.gz")).should == true - metadata_file = File.join(@full_module_name, "pkg", @release_name, "metadata.json") - File.file?(metadata_file).should == true - metadata = PSON.parse(File.read(metadata_file)) - metadata["name"].should == @full_module_name - metadata["version"].should == @version - metadata["checksums"].should be_a_kind_of(Hash) - metadata["dependencies"].should == [] - metadata["types"].should == [] - end - end - - it "should build a module's checksums" do - run do - Puppet::Module::Tool::Applications::Generator.run(@full_module_name) - Puppet::Module::Tool::Applications::Builder.run(@full_module_name) - - metadata_file = File.join(@full_module_name, "pkg", @release_name, "metadata.json") - metadata = PSON.parse(File.read(metadata_file)) - metadata["checksums"].should be_a_kind_of(Hash) - - modulefile_path = Pathname.new(File.join(@full_module_name, "Modulefile")) - metadata["checksums"]["Modulefile"].should == Digest::MD5.hexdigest(modulefile_path.read) - end - end - - it "should build a module's types and providers" do - run do - name = "jamtur01-apache" - install_release_fixture name - Puppet::Module::Tool::Applications::Builder.run(name) - - metadata_file = File.join(name, "pkg", "#{name}-0.0.1", "metadata.json") - metadata = PSON.parse(File.read(metadata_file)) - - metadata["types"].size.should == 1 - type = metadata["types"].first - type["name"].should == "a2mod" - type["doc"].should == "Manage Apache 2 modules" - - - type["parameters"].size.should == 1 - type["parameters"].first.tap do |o| - o["name"].should == "name" - o["doc"].should == "The name of the module to be managed" - end - - type["properties"].size.should == 1 - type["properties"].first.tap do |o| - o["name"].should == "ensure" - o["doc"].should =~ /present.+absent/ - end - - type["providers"].size.should == 1 - type["providers"].first.tap do |o| - o["name"].should == "debian" - o["doc"].should =~ /Manage Apache 2 modules on Debian-like OSes/ - end - end - end - - it "should build a module's dependencies" do - run do - Puppet::Module::Tool::Applications::Generator.run(@full_module_name) - modulefile = File.join(@full_module_name, "Modulefile") - - dependency1_name = "anotheruser-anothermodule" - dependency1_requirement = ">= 1.2.3" - dependency2_name = "someuser-somemodule" - dependency2_requirement = "4.2" - dependency2_repository = "http://some.repo" - - File.open(modulefile, "a") do |handle| - handle.puts "dependency '#{dependency1_name}', '#{dependency1_requirement}'" - handle.puts "dependency '#{dependency2_name}', '#{dependency2_requirement}', '#{dependency2_repository}'" - end - - Puppet::Module::Tool::Applications::Builder.run(@full_module_name) - - metadata_file = File.join(@full_module_name, "pkg", "#{@full_module_name}-#{@version}", "metadata.json") - metadata = PSON.parse(File.read(metadata_file)) - - metadata['dependencies'].size.should == 2 - metadata['dependencies'].sort_by{|t| t['name']}.tap do |dependencies| - dependencies[0].tap do |dependency1| - dependency1['name'].should == dependency1_name - dependency1['version_requirement'].should == dependency1_requirement - dependency1['repository'].should be_nil - end - - dependencies[1].tap do |dependency2| - dependency2['name'].should == dependency2_name - dependency2['version_requirement'].should == dependency2_requirement - dependency2['repository'].should == dependency2_repository - end - end - end - end - - it "should rebuild a module in a directory" do - run do - Puppet::Module::Tool::Applications::Generator.run(@full_module_name) - Puppet::Module::Tool::Applications::Builder.run(@full_module_name) - Puppet::Module::Tool::Applications::Builder.run(@full_module_name) - end - end - - it "should build a module in the current directory" do - run do - Puppet::Module::Tool::Applications::Generator.run(@full_module_name) - Dir.chdir(@full_module_name) - Puppet::Module::Tool::Applications::Builder.run(Puppet::Module::Tool.find_module_root(nil)) - - File.file?(File.join("pkg", @release_name + ".tar.gz")).should == true - end - end - - it "should fail to build a module without a Modulefile" do - run do - Puppet::Module::Tool::Applications::Generator.run(@full_module_name) - FileUtils.rm(File.join(@full_module_name, "Modulefile")) - - lambda { Puppet::Module::Tool::Applications::Builder.run(Puppet::Module::Tool.find_module_root(@full_module_name)) }.should raise_error(ArgumentError) - end - end - - it "should fail to build a module directory that doesn't exist" do - run do - lambda { Puppet::Module::Tool::Applications::Builder.run(Puppet::Module::Tool.find_module_root(@full_module_name)) }.should raise_error(ArgumentError) - end - end - - it "should fail to build a module in the current directory that's not a module" do - run do - lambda { Puppet::Module::Tool::Applications::Builder.run(Puppet::Module::Tool.find_module_root(nil)) }.should raise_error(ArgumentError) - end - end - - it "should return a Pathname object representing the path to the release archive." do - run do - Puppet::Module::Tool::Applications::Generator.run(@full_module_name) - Puppet::Module::Tool::Applications::Builder.run(@full_module_name).should be_kind_of(Pathname) - end - end - end - - describe "search" do - it "should display matching modules" do - run do - stub_repository_read 200, <<-HERE - [ - {"full_module_name": "cli", "version": "1.0"}, - {"full_module_name": "web", "version": "2.0"} - ] - HERE - Puppet::Module::Tool::Applications::Searcher.run("mymodule", @options).size.should == 2 - end - end - - it "should display no matches" do - run do - stub_repository_read 200, "[]" - Puppet::Module::Tool::Applications::Searcher.run("mymodule", @options).should == [] - end - end - - it "should fail if can't get a connection" do - run do - stub_repository_read 500, "OH NOES!!1!" - lambda { Puppet::Module::Tool::Applications::Searcher.run("mymodule", @options) }.should raise_error(RuntimeError) - end - end - - it "should return an array of module metadata hashes" do - run do - results = <<-HERE - [ - {"full_module_name": "cli", "version": "1.0"}, - {"full_module_name": "web", "version": "2.0"} - ] - HERE - expected = [ - {"version"=>"1.0", "full_module_name"=>"cli"}, - {"version"=>"2.0", "full_module_name"=>"web"} - ] - stub_repository_read 200, results - return_value = Puppet::Module::Tool::Applications::Searcher.run("mymodule", @options) - return_value.should == expected - return_value.should be_kind_of(Array) - end - end - end - - describe "install" do - it "should install a module to the puppet modulepath by default" do - myothertmpdir = Pathname.new(tmpdir("module_tool_test_myothertmpdir")) - run do - @options[:install_dir] = myothertmpdir - Puppet::Module::Tool.unstub(:install_dir) - - build_and_install_module - - File.directory?(myothertmpdir + @module_name).should == true - File.file?(myothertmpdir + @module_name + 'metadata.json').should == true - end - end - - it "should install a module from a filesystem path" do - run do - build_and_install_module - - File.directory?(@mytmpdir + @module_name).should == true - File.file?(@mytmpdir + @module_name + 'metadata.json').should == true - end - end - - it "should install a module from a webserver URL" do - run do - Puppet::Module::Tool::Applications::Generator.run(@full_module_name) - Puppet::Module::Tool::Applications::Builder.run(@full_module_name) - - stub_cache_read File.read("#{@full_module_name}/pkg/#{@release_name}.tar.gz") - FileUtils.rm_rf(@full_module_name) - - release = {"file" => "/foo/bar/#{@release_name}.tar.gz", "version" => "#{@version}"} - Puppet::Forge::Forge.any_instance.stubs(:get_release).returns(release) - - Puppet::Module::Tool::Applications::Installer.run(@full_module_name, @options) - - File.directory?(@mytmpdir + @module_name).should == true - File.file?(@mytmpdir + @module_name + 'metadata.json').should == true - end - end - - it "should install a module from a webserver URL using a version requirement" # TODO - - it "should fail if module isn't a slashed name" do - run do - lambda { Puppet::Module::Tool::Applications::Installer.run("invalid") }.should raise_error(RuntimeError) - end - end - - it "should fail if module doesn't exist on webserver" do - run do - stub_installer_read "{}" - lambda { Puppet::Module::Tool::Applications::Installer.run("not-found", @options) }.should raise_error(RuntimeError) - end - end - - it "should fail gracefully when receiving invalid PSON" do - pending "Implement PSON error wrapper" # TODO - run do - stub_installer_read "1/0" - lambda { Puppet::Module::Tool::Applications::Installer.run("not-found") }.should raise_error(SystemExit) - end - end - - it "should fail if installing a module that's already installed" do - run do - name = "myuser-mymodule" - Dir.mkdir name - lambda { Puppet::Module::Tool::Applications::Installer.run(name) }.should raise_error(ArgumentError) - end - end - - it "should return a Pathname object representing the path to the installed module" do - run do - Puppet::Module::Tool::Applications::Generator.run(@full_module_name) - Puppet::Module::Tool::Applications::Builder.run(@full_module_name) - - stub_cache_read File.read("#{@full_module_name}/pkg/#{@release_name}.tar.gz") - FileUtils.rm_rf(@full_module_name) - - release = {"file" => "/foo/bar/#{@release_name}.tar.gz", "version" => "#{@version}"} - Puppet::Forge::Forge.any_instance.stubs(:get_release).returns(release) - - Puppet::Module::Tool::Applications::Installer.run(@full_module_name, @options).should be_kind_of(Pathname) - end - end - - end - - describe "clean" do - require 'puppet/module_tool' - it "should clean cache" do - run do - build_and_install_module - Puppet::Forge::Cache.base_path.directory?.should == true - Puppet::Module::Tool::Applications::Cleaner.run - Puppet::Forge::Cache.base_path.directory?.should == false - end - end - - it "should return a status Hash" do - run do - build_and_install_module - return_value = Puppet::Module::Tool::Applications::Cleaner.run - return_value.should include(:msg) - return_value.should include(:status) - return_value.should be_kind_of(Hash) - end - end - end - - describe "changes" do - it "should return an array of modified files" do - run do - Puppet::Module::Tool::Applications::Generator.run(@full_module_name) - Puppet::Module::Tool::Applications::Builder.run(@full_module_name) - Dir.chdir("#{@full_module_name}/pkg/#{@release_name}") - File.open("Modulefile", "a") do |handle| - handle.puts - handle.puts "# Added" - end - return_value = Puppet::Module::Tool::Applications::Checksummer.run(".") - return_value.should include("Modulefile") - return_value.should be_kind_of(Array) - end - end - end -end diff --git a/spec/unit/face/module/install_spec.rb b/spec/unit/face/module/install_spec.rb new file mode 100644 index 000000000..9f67800a4 --- /dev/null +++ b/spec/unit/face/module/install_spec.rb @@ -0,0 +1,158 @@ +require 'spec_helper' +require 'puppet/face' +require 'puppet/module_tool' + +describe "puppet module install" do + + subject { Puppet::Face[:module, :current] } + + let(:options) do + {} + end + + describe "option validation" do + before do + Puppet.settings[:modulepath] = fakemodpath + end + + let(:expected_options) do + { + :target_dir => fakefirstpath, + :modulepath => fakemodpath, + :environment => 'production' + } + end + + let(:sep) { File::PATH_SEPARATOR } + let(:fakefirstpath) { "/my/fake/modpath" } + let(:fakesecondpath) { "/other/fake/path" } + let(:fakemodpath) { "#{fakefirstpath}#{sep}#{fakesecondpath}" } + let(:fakedirpath) { "/my/fake/path" } + + context "without any options" do + it "should require a name" do + pattern = /wrong number of arguments/ + expect { subject.install }.to raise_error ArgumentError, pattern + end + + it "should not require any options" do + Puppet::Module::Tool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once + subject.install("puppetlabs-apache") + end + end + + it "should accept the --force option" do + options[:force] = true + expected_options.merge!(options) + Puppet::Module::Tool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once + subject.install("puppetlabs-apache", options) + end + + it "should accept the --target-dir option" do + options[:target_dir] = "/foo/puppet/modules" + expected_options.merge!(options) + expected_options[:modulepath] = "#{options[:target_dir]}#{sep}#{fakemodpath}" + + Puppet::Module::Tool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once + subject.install("puppetlabs-apache", options) + end + + it "should accept the --version option" do + options[:version] = "0.0.1" + expected_options.merge!(options) + Puppet::Module::Tool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once + subject.install("puppetlabs-apache", options) + end + + it "should accept the --ignore-dependencies option" do + options[:ignore_dependencies] = true + expected_options.merge!(options) + Puppet::Module::Tool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once + subject.install("puppetlabs-apache", options) + end + + describe "when modulepath option is passed" do + let(:expected_options) { { :modulepath => fakemodpath, :environment => 'production' } } + let(:options) { { :modulepath => fakemodpath } } + + describe "when target-dir option is not passed" do + it "should set target-dir to be first path from modulepath" do + expected_options[:target_dir] = fakefirstpath + + Puppet::Module::Tool::Applications::Installer. + expects(:run). + with("puppetlabs-apache", expected_options) + + Puppet::Face[:module, :current].install("puppetlabs-apache", options) + + Puppet.settings[:modulepath].should == fakemodpath + end + end + + describe "when target-dir option is passed" do + it "should set target-dir to be first path of modulepath" do + options[:target_dir] = fakedirpath + expected_options[:target_dir] = fakedirpath + expected_options[:modulepath] = "#{fakedirpath}#{sep}#{fakemodpath}" + + Puppet::Module::Tool::Applications::Installer. + expects(:run). + with("puppetlabs-apache", expected_options) + + Puppet::Face[:module, :current].install("puppetlabs-apache", options) + + Puppet.settings[:modulepath].should == "#{fakedirpath}#{sep}#{fakemodpath}" + end + end + end + + describe "when modulepath option is not passed" do + before do + Puppet.settings[:modulepath] = fakemodpath + end + + describe "when target-dir option is not passed" do + it "should set target-dir to be first path of default mod path" do + expected_options[:target_dir] = fakefirstpath + expected_options[:modulepath] = fakemodpath + + Puppet::Module::Tool::Applications::Installer. + expects(:run). + with("puppetlabs-apache", expected_options) + + Puppet::Face[:module, :current].install("puppetlabs-apache", options) + end + end + + describe "when target-dir option is passed" do + it "should prepend target-dir to modulepath" do + options[:target_dir] = fakedirpath + expected_options[:target_dir] = fakedirpath + expected_options[:modulepath] = "#{options[:target_dir]}#{sep}#{fakemodpath}" + + Puppet::Module::Tool::Applications::Installer. + expects(:run). + with("puppetlabs-apache", expected_options) + + Puppet::Face[:module, :current].install("puppetlabs-apache", options) + Puppet.settings[:modulepath].should == expected_options[:modulepath] + end + end + end + end + + describe "inline documentation" do + subject { Puppet::Face[:module, :current].get_action :install } + + its(:summary) { should =~ /install.*module/im } + its(:description) { should =~ /install.*module/im } + its(:returns) { should =~ /pathname/i } + its(:examples) { should_not be_empty } + + %w{ license copyright summary description returns examples }.each do |doc| + context "of the" do + its(doc.to_sym) { should_not =~ /(FIXME|REVISIT|TODO)/ } + end + end + end +end diff --git a/spec/unit/face/module/list_spec.rb b/spec/unit/face/module/list_spec.rb new file mode 100644 index 000000000..fa1636a4b --- /dev/null +++ b/spec/unit/face/module/list_spec.rb @@ -0,0 +1,182 @@ +# encoding: UTF-8 + +require 'spec_helper' +require 'puppet/face' +require 'puppet/module_tool' +require 'puppet_spec/modules' + +describe "puppet module list", :fails_on_windows => true do + include PuppetSpec::Files + + before do + dir = tmpdir("deep_path") + + @modpath1 = File.join(dir, "modpath1") + @modpath2 = File.join(dir, "modpath2") + @modulepath = "#{@modpath1}#{File::PATH_SEPARATOR}#{@modpath2}" + Puppet.settings[:modulepath] = @modulepath + + FileUtils.mkdir_p(@modpath1) + FileUtils.mkdir_p(@modpath2) + end + + it "should return an empty list per dir in path if there are no modules" do + Puppet.settings[:modulepath] = @modulepath + Puppet::Face[:module, :current].list.should == { + @modpath1 => [], + @modpath2 => [] + } + end + + it "should include modules separated by the environment's modulepath" do + foomod1 = PuppetSpec::Modules.create('foo', @modpath1) + barmod1 = PuppetSpec::Modules.create('bar', @modpath1) + foomod2 = PuppetSpec::Modules.create('foo', @modpath2) + + env = Puppet::Node::Environment.new + + Puppet::Face[:module, :current].list.should == { + @modpath1 => [ + Puppet::Module.new('bar', :environment => env, :path => barmod1.path), + Puppet::Module.new('foo', :environment => env, :path => foomod1.path) + ], + @modpath2 => [Puppet::Module.new('foo', :environment => env, :path => foomod2.path)] + } + end + + it "should use the specified environment" do + PuppetSpec::Modules.create('foo', @modpath1) + PuppetSpec::Modules.create('bar', @modpath1) + + usedenv = Puppet::Node::Environment.new('useme') + usedenv.modulepath = [@modpath1, @modpath2] + + Puppet::Face[:module, :current].list(:environment => 'useme').should == { + @modpath1 => [ + Puppet::Module.new('bar', :environment => usedenv), + Puppet::Module.new('foo', :environment => usedenv) + ], + @modpath2 => [] + } + end + + it "should use the specified modulepath" do + PuppetSpec::Modules.create('foo', @modpath1) + PuppetSpec::Modules.create('bar', @modpath2) + + Puppet::Face[:module, :current].list(:modulepath => "#{@modpath1}#{File::PATH_SEPARATOR}#{@modpath2}").should == { + @modpath1 => [ Puppet::Module.new('foo') ], + @modpath2 => [ Puppet::Module.new('bar') ] + } + end + + it "should use the specified modulepath over the specified environment in place of the environment's default path" do + foomod1 = PuppetSpec::Modules.create('foo', @modpath1) + barmod2 = PuppetSpec::Modules.create('bar', @modpath2) + env = Puppet::Node::Environment.new('myenv') + env.modulepath = ['/tmp/notused'] + + list = Puppet::Face[:module, :current].list(:environment => 'myenv', :modulepath => "#{@modpath1}#{File::PATH_SEPARATOR}#{@modpath2}") + + # Changing Puppet[:modulepath] causes Puppet::Node::Environment.new('myenv') + # to have a different object_id than the env above + env = Puppet::Node::Environment.new('myenv') + list.should == { + @modpath1 => [ Puppet::Module.new('foo', :environment => env, :path => foomod1.path) ], + @modpath2 => [ Puppet::Module.new('bar', :environment => env, :path => barmod2.path) ] + } + end + + describe "inline documentation" do + subject { Puppet::Face[:module, :current].get_action :list } + + its(:summary) { should =~ /list.*module/im } + its(:description) { should =~ /list.*module/im } + its(:returns) { should =~ /hash of paths to module objects/i } + its(:examples) { should_not be_empty } + end + + describe "when rendering" do + it "should explicitly state when a modulepath is empty" do + empty_modpath = tmpdir('empty') + Puppet::Face[:module, :current].list_when_rendering_console( + { empty_modpath => [] }, + {:modulepath => empty_modpath} + ).should == <<-HEREDOC.gsub(' ', '') + #{empty_modpath} (no modules installed) + HEREDOC + end + + it "should print both modules with and without metadata" do + modpath = tmpdir('modpath') + Puppet.settings[:modulepath] = modpath + PuppetSpec::Modules.create('nometadata', modpath) + PuppetSpec::Modules.create('metadata', modpath, :metadata => {:author => 'metaman'}) + + dependency_tree = Puppet::Face[:module, :current].list + + output = Puppet::Face[:module, :current].list_when_rendering_console( + dependency_tree, + {} + ) + + output.should == <<-HEREDOC.gsub(' ', '') + #{modpath} + ├── metaman-metadata (\e[0;36mv9.9.9\e[0m) + └── nometadata (\e[0;36m???\e[0m) + HEREDOC + end + + it "should print the modulepaths in the order they are in the modulepath setting" do + path1 = tmpdir('b') + path2 = tmpdir('c') + path3 = tmpdir('a') + + sep = File::PATH_SEPARATOR + Puppet.settings[:modulepath] = "#{path1}#{sep}#{path2}#{sep}#{path3}" + + Puppet::Face[:module, :current].list_when_rendering_console( + { + path2 => [], + path3 => [], + path1 => [], + }, + {} + ).should == <<-HEREDOC.gsub(' ', '') + #{path1} (no modules installed) + #{path2} (no modules installed) + #{path3} (no modules installed) + HEREDOC + end + + it "should print dependencies as a tree" do + PuppetSpec::Modules.create('dependable', @modpath1, :metadata => { :version => '0.0.5'}) + PuppetSpec::Modules.create( + 'other_mod', + @modpath1, + :metadata => { + :version => '1.0.0', + :dependencies => [{ + "version_requirement" => ">= 0.0.5", + "name" => "puppetlabs/dependable" + }] + } + ) + + dependency_tree = Puppet::Face[:module, :current].list + + output = Puppet::Face[:module, :current].list_when_rendering_console( + dependency_tree, + {:tree => true} + ) + + output.should == <<-HEREDOC.gsub(' ', '') + #{@modpath1} + └─┬ puppetlabs-other_mod (\e[0;36mv1.0.0\e[0m) + └── puppetlabs-dependable (\e[0;36mv0.0.5\e[0m) + #{@modpath2} (no modules installed) + HEREDOC + end + end + +end diff --git a/spec/unit/face/module/search_spec.rb b/spec/unit/face/module/search_spec.rb new file mode 100644 index 000000000..51f62bd1f --- /dev/null +++ b/spec/unit/face/module/search_spec.rb @@ -0,0 +1,163 @@ +require 'spec_helper' +require 'puppet/face' +require 'puppet/application/module' +require 'puppet/module_tool' + +describe "puppet module search", :fails_on_windows => true do + subject { Puppet::Face[:module, :current] } + + let(:options) do + {} + end + + describe Puppet::Application::Module do + subject do + app = Puppet::Application::Module.new + app.stubs(:action).returns(Puppet::Face.find_action(:module, :search)) + app + end + + before { subject.render_as = :console } + before { Puppet::Util::Terminal.stubs(:width).returns(100) } + + it 'should output nothing when receiving an empty dataset' do + subject.render([], ['apache', {}]).should == "No results found for 'apache'." + end + + it 'should output a header when receiving a non-empty dataset' do + results = [ + {'full_name' => '', 'author' => '', 'desc' => '', 'tag_list' => [] }, + ] + + subject.render(results, ['apache', {}]).should =~ /NAME/ + subject.render(results, ['apache', {}]).should =~ /DESCRIPTION/ + subject.render(results, ['apache', {}]).should =~ /AUTHOR/ + subject.render(results, ['apache', {}]).should =~ /KEYWORDS/ + end + + it 'should output the relevant fields when receiving a non-empty dataset' do + results = [ + {'full_name' => 'Name', 'author' => 'Author', 'desc' => 'Summary', 'tag_list' => ['tag1', 'tag2'] }, + ] + + subject.render(results, ['apache', {}]).should =~ /Name/ + subject.render(results, ['apache', {}]).should =~ /Author/ + subject.render(results, ['apache', {}]).should =~ /Summary/ + subject.render(results, ['apache', {}]).should =~ /tag1/ + subject.render(results, ['apache', {}]).should =~ /tag2/ + end + + it 'should elide really long descriptions' do + results = [ + { + 'full_name' => 'Name', + 'author' => 'Author', + 'desc' => 'This description is really too long to fit in a single data table, guys -- we should probably set about truncating it', + 'tag_list' => ['tag1', 'tag2'], + }, + ] + + subject.render(results, ['apache', {}]).should =~ /\.{3} @Author/ + end + + it 'should never truncate the module name' do + results = [ + { + 'full_name' => 'This-module-has-a-really-really-long-name', + 'author' => 'Author', + 'desc' => 'Description', + 'tag_list' => ['tag1', 'tag2'], + }, + ] + + subject.render(results, ['apache', {}]).should =~ /This-module-has-a-really-really-long-name/ + end + + it 'should never truncate the author name' do + results = [ + { + 'full_name' => 'Name', + 'author' => 'This-author-has-a-really-really-long-name', + 'desc' => 'Description', + 'tag_list' => ['tag1', 'tag2'], + }, + ] + + subject.render(results, ['apache', {}]).should =~ /@This-author-has-a-really-really-long-name/ + end + + it 'should never remove tags that match the search term' do + results = [ + { + 'full_name' => 'Name', + 'author' => 'Author', + 'desc' => 'Description', + 'tag_list' => ['Supercalifragilisticexpialidocious'] + (1..100).map { |i| "tag#{i}" }, + }, + ] + + subject.render(results, ['Supercalifragilisticexpialidocious', {}]).should =~ /Supercalifragilisticexpialidocious/ + subject.render(results, ['Supercalifragilisticexpialidocious', {}]).should_not =~ /tag/ + end + + { + 100 => "NAME DESCRIPTION AUTHOR KEYWORDS#{' '*15}\n"\ + "Name This description is really too long to fit ... @JohnnyApples tag1 tag2 taggitty3#{' '*4}\n", + + 70 => "NAME DESCRIPTION AUTHOR KEYWORDS#{' '*5}\n"\ + "Name This description is rea... @JohnnyApples tag1 tag2#{' '*4}\n", + + 80 => "NAME DESCRIPTION AUTHOR KEYWORDS#{' '*8}\n"\ + "Name This description is really too... @JohnnyApples tag1 tag2#{' '*7}\n", + + 200 => "NAME DESCRIPTION AUTHOR KEYWORDS#{' '*48}\n"\ + "Name This description is really too long to fit in a single data table, guys -- we should probably set about trunca... @JohnnyApples tag1 tag2 taggitty3#{' '*37}\n" + }.each do |width, expectation| + it "should resize the table to fit the screen, when #{width} columns" do + results = [ + { + 'full_name' => 'Name', + 'author' => 'JohnnyApples', + 'desc' => 'This description is really too long to fit in a single data table, guys -- we should probably set about truncating it', + 'tag_list' => ['tag1', 'tag2', 'taggitty3'], + }, + ] + + Puppet::Util::Terminal.expects(:width).returns(width) + result = subject.render(results, ['apache', {}]) + result.lines.sort_by(&:length).last.chomp.length.should <= width + result.should == expectation + end + end + end + + describe "option validation" do + context "without any options" do + it "should require a search term" do + pattern = /wrong number of arguments/ + expect { subject.search }.to raise_error ArgumentError, pattern + end + end + + it "should accept the --module-repository option" do + options[:module_repository] = "http://forge.example.com" + Puppet::Module::Tool::Applications::Searcher.expects(:run).with("puppetlabs-apache", options).once + subject.search("puppetlabs-apache", options) + end + end + + describe "inline documentation" do + subject { Puppet::Face[:module, :current].get_action :search } + + its(:summary) { should =~ /search.*module/im } + its(:description) { should =~ /search.*module/im } + its(:returns) { should =~ /array/i } + its(:examples) { should_not be_empty } + + %w{ license copyright summary description returns examples }.each do |doc| + context "of the" do + its(doc.to_sym) { should_not =~ /(FIXME|REVISIT|TODO)/ } + end + end + end +end diff --git a/spec/unit/face/module/uninstall_spec.rb b/spec/unit/face/module/uninstall_spec.rb new file mode 100644 index 000000000..a157df509 --- /dev/null +++ b/spec/unit/face/module/uninstall_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' +require 'puppet/face' +require 'puppet/module_tool' + +describe "puppet module uninstall" do + subject { Puppet::Face[:module, :current] } + + let(:options) do + {} + end + + describe "option validation" do + context "without any options" do + it "should require a name" do + pattern = /wrong number of arguments/ + expect { subject.uninstall }.to raise_error ArgumentError, pattern + end + + it "should not require any options" do + Puppet::Module::Tool::Applications::Uninstaller.expects(:run).once + subject.uninstall("puppetlabs-apache") + end + end + + it "should accept the --environment option" do + options[:environment] = "development" + expected_options = { :environment => 'development' } + Puppet::Module::Tool::Applications::Uninstaller.expects(:run).with("puppetlabs-apache", expected_options).once + subject.uninstall("puppetlabs-apache", options) + end + + it "should accept the --modulepath option" do + options[:modulepath] = "/foo/puppet/modules" + expected_options = { + :modulepath => '/foo/puppet/modules', + :environment => 'production', + } + Puppet::Module::Tool::Applications::Uninstaller.expects(:run).with("puppetlabs-apache", expected_options).once + subject.uninstall("puppetlabs-apache", options) + end + + it "should accept the --version option" do + options[:version] = "1.0.0" + expected_options = { + :version => '1.0.0', + :environment => 'production', + } + Puppet::Module::Tool::Applications::Uninstaller.expects(:run).with("puppetlabs-apache", expected_options).once + subject.uninstall("puppetlabs-apache", options) + end + + it "should accept the --force flag" do + options[:force] = true + expected_options = { + :environment => 'production', + :force => true, + } + Puppet::Module::Tool::Applications::Uninstaller.expects(:run).with("puppetlabs-apache", expected_options).once + subject.uninstall("puppetlabs-apache", options) + end + end + + describe "inline documentation" do + subject { Puppet::Face[:module, :current].get_action :uninstall } + + its(:summary) { should =~ /uninstall.*module/im } + its(:description) { should =~ /uninstall.*module/im } + its(:returns) { should =~ /hash of module objects.*/im } + its(:examples) { should_not be_empty } + + %w{ license copyright summary description returns examples }.each do |doc| + context "of the" do + its(doc.to_sym) { should_not =~ /(FIXME|REVISIT|TODO)/ } + end + end + end +end diff --git a/spec/unit/face/module/upgrade_spec.rb b/spec/unit/face/module/upgrade_spec.rb new file mode 100644 index 000000000..c7c2bbcef --- /dev/null +++ b/spec/unit/face/module/upgrade_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' +require 'puppet/face' +require 'puppet/module_tool' + +describe "puppet module upgrade" do + subject { Puppet::Face[:module, :current] } + + let(:options) do + {} + end + + describe "inline documentation" do + subject { Puppet::Face[:module, :current].get_action :upgrade } + + its(:summary) { should =~ /upgrade.*module/im } + its(:description) { should =~ /upgrade.*module/im } + its(:returns) { should =~ /hash/i } + its(:examples) { should_not be_empty } + + %w{ license copyright summary description returns examples }.each do |doc| + context "of the" do + its(doc.to_sym) { should_not =~ /(FIXME|REVISIT|TODO)/ } + end + end + end +end diff --git a/spec/unit/forge/repository_spec.rb b/spec/unit/forge/repository_spec.rb index 6d8ce38f1..bbfc0d136 100644 --- a/spec/unit/forge/repository_spec.rb +++ b/spec/unit/forge/repository_spec.rb @@ -1,86 +1,56 @@ require 'spec_helper' require 'net/http' require 'puppet/forge/repository' require 'puppet/forge/cache' describe Puppet::Forge::Repository do describe 'instances' do let(:repository) { Puppet::Forge::Repository.new('http://fake.com') } - describe '#make_http_request' do - before do - # Do a mock of the Proxy call so we can do proper expects for - # Net::HTTP - Net::HTTP.expects(:Proxy).returns(Net::HTTP) - Net::HTTP.expects(:start) - end - context "when not given an :authenticate option" do - it "should authenticate" do - repository.expects(:authenticate).never - repository.make_http_request(nil) - end - end - context "when given an :authenticate option" do - it "should authenticate" do - repository.expects(:authenticate) - repository.make_http_request(nil, :authenticate => true) - end - end - end - - describe '#authenticate' do - it "should set basic auth on the request" do - authenticated_request = stub - authenticated_request.expects(:basic_auth) - repository.expects(:prompt).twice - repository.authenticate(authenticated_request) - end - end - describe '#retrieve' do before do @uri = URI.parse('http://some.url.com') end it "should access the cache" do repository.cache.expects(:retrieve).with(@uri) repository.retrieve(@uri) end end describe 'http_proxy support' do before :each do ENV["http_proxy"] = nil end after :each do ENV["http_proxy"] = nil end it "should support environment variable for port and host" do ENV["http_proxy"] = "http://test.com:8011" repository.http_proxy_host.should == "test.com" repository.http_proxy_port.should == 8011 end it "should support puppet configuration for port and host" do ENV["http_proxy"] = nil Puppet.settings.stubs(:[]).with(:http_proxy_host).returns('test.com') Puppet.settings.stubs(:[]).with(:http_proxy_port).returns(7456) repository.http_proxy_port.should == 7456 repository.http_proxy_host.should == "test.com" end it "should use environment variable before puppet settings" do ENV["http_proxy"] = "http://test1.com:8011" Puppet.settings.stubs(:[]).with(:http_proxy_host).returns('test2.com') Puppet.settings.stubs(:[]).with(:http_proxy_port).returns(7456) repository.http_proxy_host.should == "test1.com" repository.http_proxy_port.should == 8011 end end end end diff --git a/spec/unit/forge_spec.rb b/spec/unit/forge_spec.rb index 905f1bd24..95e47a03e 100644 --- a/spec/unit/forge_spec.rb +++ b/spec/unit/forge_spec.rb @@ -1,114 +1,56 @@ require 'spec_helper' require 'puppet/forge' require 'net/http' +require 'puppet/module_tool' + +describe Puppet::Forge do + include PuppetSpec::Files + + let(:response_body) do + <<-EOF + [ + { + "author": "puppetlabs", + "name": "bacula", + "tag_list": ["backup", "bacula"], + "releases": [{"version": "0.0.1"}, {"version": "0.0.2"}], + "full_name": "puppetlabs/bacula", + "version": "0.0.2", + "project_url": "http://github.com/puppetlabs/puppetlabs-bacula", + "desc": "bacula" + } + ] + EOF + end + let(:response) { stub(:body => response_body, :code => '200') } -describe Puppet::Forge::Forge do before do Puppet::Forge::Repository.any_instance.stubs(:make_http_request).returns(response) Puppet::Forge::Repository.any_instance.stubs(:retrieve).returns("/tmp/foo") end - let(:forge) { forge = Puppet::Forge::Forge.new('http://forge.puppetlabs.com') } - describe "the behavior of the search method" do context "when there are matches for the search term" do before do Puppet::Forge::Repository.any_instance.stubs(:make_http_request).returns(response) end - let(:response) { stub(:body => response_body, :code => '200') } - let(:response_body) do - <<-EOF - [ - { - "author": "puppetlabs", - "name": "bacula", - "tag_list": ["backup", "bacula"], - "releases": [{"version": "0.0.1"}, {"version": "0.0.2"}], - "full_name": "puppetlabs/bacula", - "version": "0.0.2", - "project_url": "http://github.com/puppetlabs/puppetlabs-bacula", - "desc": "bacula" - } - ] - EOF - end - it "should return a list of matches from the forge" do - forge.search('bacula').should == PSON.load(response_body) + Puppet::Forge.search('bacula').should == PSON.load(response_body) end end context "when the connection to the forge fails" do - let(:response) { stub(:body => '[]', :code => '404') } + let(:response) { stub(:body => '{}', :code => '404') } - it "should raise an error" do - lambda { forge.search('bacula') }.should raise_error RuntimeError + it "should raise an error for search" do + lambda { Puppet::Forge.search('bacula') }.should raise_error RuntimeError end - end - end - - describe "the behavior of the get_release_package method" do - let(:response) do - response = mock() - response.stubs(:body).returns('{"file": "/system/releases/p/puppetlabs/puppetlabs-apache-0.0.3.tar.gz", "version": "0.0.3"}') - response - end - - context "when source is not filesystem or repository" do - it "should raise an error" do - params = { :source => 'foo' } - lambda { forge.get_release_package(params) }.should - raise_error(ArgumentError, "Could not determine installation source") - end - end - - context "when the source is a repository" do - let(:params) do - { - :source => :repository, - :author => 'fakeauthor', - :modname => 'fakemodule', - :version => '0.0.1' - } - end - - it "should require author" do - params.delete(:author) - lambda { forge.get_release_package(params) }.should - raise_error(ArgumentError, ":author and :modename required") - end - - it "should require modname" do - params.delete(:modname) - lambda { forge.get_release_package(params) }.should - raise_error(ArgumentError, ":author and :modename required") - end - - it "should download the release package" do - forge.get_release_package(params).should == "/tmp/foo" - end - end - - context "when the source is a filesystem" do - it "should require filename" do - params = { :source => :filesystem } - lambda { forge.get_release_package(params) }.should - raise_error(ArgumentError, ":filename required") + it "should raise an error for remote_dependency_info" do + lambda { Puppet::Forge.remote_dependency_info('puppetlabs', 'bacula', '0.0.1') }.should raise_error RuntimeError end end end - describe "the behavior of the get_releases method" do - let(:response) do - response = mock() - response.stubs(:body).returns('{"releases": [{"version": "0.0.1"}, {"version": "0.0.2"}, {"version": "0.0.3"}]}') - response - end - - it "should return a list of module releases" do - forge.get_releases('fakeauthor', 'fakemodule').should == ["0.0.1", "0.0.2", "0.0.3"] - end - end end diff --git a/spec/unit/module_spec.rb b/spec/unit/module_spec.rb index 67f36167b..bb32ccdde 100755 --- a/spec/unit/module_spec.rb +++ b/spec/unit/module_spec.rb @@ -1,722 +1,855 @@ #!/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 it "should return the path to the plugin directory" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" mod.plugin_directory.should == "/a/foo/lib" 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 Puppet.expects(:deprecation_warning).with("using the deprecated 'plugins' directory for ruby extensions; please move to 'lib'") mod.plugin_directory.should == "/a/foo/plugins" 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..1517f30f5 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 '.format_tree' do + it 'should return an empty tree when given an empty list' do + subject.format_tree([]).should == '' + end + + it 'should return a shallow when given a list without dependencies' do + list = [ { :text => 'first' }, { :text => 'second' }, { :text => 'third' } ] + subject.format_tree(list).should == <<-TREE +├── first +├── second +└── third +TREE + end + + it 'should return a deeply nested tree when given a list with deep dependencies' do + list = [ + { + :text => 'first', + :dependencies => [ + { + :text => 'second', + :dependencies => [ + { :text => 'third' } + ] + } + ] + }, + ] + subject.format_tree(list).should == <<-TREE +└─┬ first + └─┬ second + └── third +TREE + end + + it 'should show connectors when deep dependencies are not on the last node of the top level' do + list = [ + { + :text => 'first', + :dependencies => [ + { + :text => 'second', + :dependencies => [ + { :text => 'third' } + ] + } + ] + }, + { :text => 'fourth' } + ] + subject.format_tree(list).should == <<-TREE +├─┬ first +│ └─┬ second +│ └── third +└── fourth +TREE + end + + it 'should show connectors when deep dependencies are not on the last node of any level' do + list = [ + { + :text => 'first', + :dependencies => [ + { + :text => 'second', + :dependencies => [ + { :text => 'third' } + ] + }, + { :text => 'fourth' } + ] + } + ] + subject.format_tree(list).should == <<-TREE +└─┬ first + ├─┬ second + │ └── third + └── fourth +TREE + end + + it 'should show connectors in every case when deep dependencies are not on the last node' do + list = [ + { + :text => 'first', + :dependencies => [ + { + :text => 'second', + :dependencies => [ + { :text => 'third' } + ] + }, + { :text => 'fourth' } + ] + }, + { :text => 'fifth' } + ] + subject.format_tree(list).should == <<-TREE +├─┬ first +│ ├─┬ second +│ │ └── third +│ └── fourth +└── fifth +TREE + end + end end diff --git a/spec/unit/node/environment_spec.rb b/spec/unit/node/environment_spec.rb index 17d55bf28..34afc4449 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.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/parser/functions/create_resources_spec.rb b/spec/unit/parser/functions/create_resources_spec.rb index cdf61b84b..6301c3ea9 100755 --- a/spec/unit/parser/functions/create_resources_spec.rb +++ b/spec/unit/parser/functions/create_resources_spec.rb @@ -1,157 +1,171 @@ require 'puppet' require 'spec_helper' describe 'function for dynamically creating resources' do def get_scope @topscope = Puppet::Parser::Scope.new # This is necessary so we don't try to use the compiler to discover our parent. @topscope.parent = nil @scope = Puppet::Parser::Scope.new @scope.compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("floppy", :environment => 'production')) @scope.parent = @topscope @compiler = @scope.compiler end before :each do get_scope Puppet::Parser::Functions.function(:create_resources) end it "should exist" do Puppet::Parser::Functions.function(:create_resources).should == "function_create_resources" end it 'should require two or three arguments' do expect { @scope.function_create_resources(['foo']) }.should raise_error(ArgumentError, 'create_resources(): wrong number of arguments (1; must be 2 or 3)') expect { @scope.function_create_resources(['foo', 'bar', 'blah', 'baz']) }.should raise_error(ArgumentError, 'create_resources(): wrong number of arguments (4; must be 2 or 3)') end + describe 'when the caller does not supply a name parameter' do + it 'should set a default resource name equal to the resource title' do + Puppet::Parser::Resource.any_instance.expects(:set_parameter).with(:name, 'test').once + @scope.function_create_resources(['notify', {'test'=>{}}]) + end + end + describe 'when the caller supplies a name parameter' do + it 'should set the resource name to the value provided' do + Puppet::Parser::Resource.any_instance.expects(:set_parameter).with(:name, 'user_supplied').once + Puppet::Parser::Resource.any_instance.expects(:set_parameter).with(:name, 'test').never + @scope.function_create_resources(['notify', {'test'=>{'name' => 'user_supplied'}}]) + end + end + describe 'when creating native types' do before :each do Puppet[:code]='notify{test:}' get_scope @scope.resource=Puppet::Parser::Resource.new('class', 't', :scope => @scope) end it 'empty hash should not cause resources to be added' do @scope.function_create_resources(['file', {}]) @compiler.catalog.resources.size == 1 end it 'should be able to add' do @scope.function_create_resources(['file', {'/etc/foo'=>{'ensure'=>'present'}}]) @compiler.catalog.resource(:file, "/etc/foo")['ensure'].should == 'present' end it 'should accept multiple types' do type_hash = {} type_hash['foo'] = {'message' => 'one'} type_hash['bar'] = {'message' => 'two'} @scope.function_create_resources(['notify', type_hash]) @compiler.catalog.resource(:notify, "foo")['message'].should == 'one' @compiler.catalog.resource(:notify, "bar")['message'].should == 'two' end it 'should fail to add non-existing type' do expect { @scope.function_create_resources(['create-resource-foo', {}]) }.should raise_error(ArgumentError, 'could not create resource of unknown type create-resource-foo') end it 'should be able to add edges' do @scope.function_create_resources(['notify', {'foo'=>{'require' => 'Notify[test]'}}]) @scope.compiler.compile rg = @scope.compiler.catalog.to_ral.relationship_graph test = rg.vertices.find { |v| v.title == 'test' } foo = rg.vertices.find { |v| v.title == 'foo' } test.should be foo.should be rg.path_between(test,foo).should be end it 'should account for default values' do @scope.function_create_resources(['file', {'/etc/foo'=>{'ensure'=>'present'}, '/etc/baz'=>{'group'=>'food'}}, {'group' => 'bar'}]) @compiler.catalog.resource(:file, "/etc/foo")['group'].should == 'bar' @compiler.catalog.resource(:file, "/etc/baz")['group'].should == 'food' end end describe 'when dynamically creating resource types' do before :each do Puppet[:code]= 'define foocreateresource($one){notify{$name: message => $one}} notify{test:} ' get_scope @scope.resource=Puppet::Parser::Resource.new('class', 't', :scope => @scope) Puppet::Parser::Functions.function(:create_resources) end it 'should be able to create defined resoure types' do @scope.function_create_resources(['foocreateresource', {'blah'=>{'one'=>'two'}}]) # still have to compile for this to work... # I am not sure if this constraint ruins the tests @scope.compiler.compile @compiler.catalog.resource(:notify, "blah")['message'].should == 'two' end it 'should fail if defines are missing params' do @scope.function_create_resources(['foocreateresource', {'blah'=>{}}]) expect { @scope.compiler.compile }.should raise_error(Puppet::ParseError, 'Must pass one to Foocreateresource[blah]') end it 'should be able to add multiple defines' do hash = {} hash['blah'] = {'one' => 'two'} hash['blaz'] = {'one' => 'three'} @scope.function_create_resources(['foocreateresource', hash]) # still have to compile for this to work... # I am not sure if this constraint ruins the tests @scope.compiler.compile @compiler.catalog.resource(:notify, "blah")['message'].should == 'two' @compiler.catalog.resource(:notify, "blaz")['message'].should == 'three' end it 'should be able to add edges' do @scope.function_create_resources(['foocreateresource', {'blah'=>{'one'=>'two', 'require' => 'Notify[test]'}}]) @scope.compiler.compile rg = @scope.compiler.catalog.to_ral.relationship_graph test = rg.vertices.find { |v| v.title == 'test' } blah = rg.vertices.find { |v| v.title == 'blah' } test.should be blah.should be # (Yoda speak like we do) rg.path_between(test,blah).should be @compiler.catalog.resource(:notify, "blah")['message'].should == 'two' end it 'should account for default values' do @scope.function_create_resources(['foocreateresource', {'blah'=>{}}, {'one' => 'two'}]) @scope.compiler.compile @compiler.catalog.resource(:notify, "blah")['message'].should == 'two' end end describe 'when creating classes' do before :each do Puppet[:code]= 'class bar($one){notify{test: message => $one}} notify{tester:} ' get_scope @scope.resource=Puppet::Parser::Resource.new('class', 't', :scope => @scope) Puppet::Parser::Functions.function(:create_resources) end it 'should be able to create classes' do @scope.function_create_resources(['class', {'bar'=>{'one'=>'two'}}]) @scope.compiler.compile @compiler.catalog.resource(:notify, "test")['message'].should == 'two' @compiler.catalog.resource(:class, "bar").should_not be_nil end it 'should fail to create non-existing classes' do expect { @scope.function_create_resources(['class', {'blah'=>{'one'=>'two'}}]) }.should raise_error(ArgumentError ,'could not find hostclass blah') end it 'should be able to add edges' do @scope.function_create_resources(['class', {'bar'=>{'one'=>'two', 'require' => 'Notify[tester]'}}]) @scope.compiler.compile rg = @scope.compiler.catalog.to_ral.relationship_graph test = rg.vertices.find { |v| v.title == 'test' } tester = rg.vertices.find { |v| v.title == 'tester' } test.should be tester.should be rg.path_between(tester,test).should be end it 'should account for default values' do @scope.function_create_resources(['class', {'bar'=>{}}, {'one' => 'two'}]) @scope.compiler.compile @compiler.catalog.resource(:notify, "test")['message'].should == 'two' @compiler.catalog.resource(:class, "bar").should_not be_nil end end end diff --git a/spec/unit/provider/nameservice/directoryservice_spec.rb b/spec/unit/provider/nameservice/directoryservice_spec.rb index e3d32d713..a09894385 100755 --- a/spec/unit/provider/nameservice/directoryservice_spec.rb +++ b/spec/unit/provider/nameservice/directoryservice_spec.rb @@ -1,179 +1,189 @@ #!/usr/bin/env rspec require 'spec_helper' # We use this as a reasonable way to obtain all the support infrastructure. [:user, :group].each do |type_for_this_round| provider_class = Puppet::Type.type(type_for_this_round).provider(:directoryservice) describe provider_class do before do @resource = stub("resource") @provider = provider_class.new(@resource) end it "[#6009] should handle nested arrays of members" do current = ["foo", "bar", "baz"] desired = ["foo", ["quux"], "qorp"] group = 'example' @resource.stubs(:[]).with(:name).returns(group) @resource.stubs(:[]).with(:auth_membership).returns(true) @provider.instance_variable_set(:@property_value_cache_hash, { :members => current }) %w{bar baz}.each do |del| @provider.expects(:execute).once. with([:dseditgroup, '-o', 'edit', '-n', '.', '-d', del, group]) end %w{quux qorp}.each do |add| @provider.expects(:execute).once. with([:dseditgroup, '-o', 'edit', '-n', '.', '-a', add, group]) end expect { @provider.set(:members, desired) }.should_not raise_error end end end describe 'DirectoryService.single_report' do it 'should fail on OS X < 10.5' do Puppet::Provider::NameService::DirectoryService.stubs(:get_macosx_version_major).returns("10.4") lambda { Puppet::Provider::NameService::DirectoryService.single_report('resource_name') }.should raise_error(RuntimeError, "Puppet does not support OS X versions < 10.5") end it 'should use plist data on >= 10.5' do Puppet::Provider::NameService::DirectoryService.stubs(:get_macosx_version_major).returns("10.5") Puppet::Provider::NameService::DirectoryService.stubs(:get_ds_path).returns('Users') Puppet::Provider::NameService::DirectoryService.stubs(:list_all_present).returns( ['root', 'user1', 'user2', 'resource_name'] ) Puppet::Provider::NameService::DirectoryService.stubs(:generate_attribute_hash) Puppet::Provider::NameService::DirectoryService.stubs(:execute) Puppet::Provider::NameService::DirectoryService.expects(:parse_dscl_plist_data) Puppet::Provider::NameService::DirectoryService.single_report('resource_name') end end describe 'DirectoryService.get_exec_preamble' do it 'should fail on OS X < 10.5' do Puppet::Provider::NameService::DirectoryService.stubs(:get_macosx_version_major).returns("10.4") lambda { Puppet::Provider::NameService::DirectoryService.get_exec_preamble('-list') }.should raise_error(RuntimeError, "Puppet does not support OS X versions < 10.5") end it 'should use plist data on >= 10.5' do Puppet::Provider::NameService::DirectoryService.stubs(:get_macosx_version_major).returns("10.5") Puppet::Provider::NameService::DirectoryService.stubs(:get_ds_path).returns('Users') Puppet::Provider::NameService::DirectoryService.get_exec_preamble('-list').should include("-plist") end end describe 'DirectoryService password behavior' do # The below is a binary plist containing a ShadowHashData key which CONTAINS # another binary plist. The nested binary plist contains a 'SALTED-SHA512' # key that contains a base64 encoded salted-SHA512 password hash... let (:binary_plist) { "bplist00\324\001\002\003\004\005\006\a\bXCRAM-MD5RNT]SALTED-SHA512[RECOVERABLEO\020 \231k2\3360\200GI\201\355J\216\202\215y\243\001\206J\300\363\032\031\022\006\2359\024\257\217<\361O\020\020F\353\at\377\277\226\276c\306\254\031\037J(\235O\020D\335\006{\3744g@\377z\204\322\r\332t\021\330\n\003\246K\223\356\034!P\261\305t\035\346\352p\206\003n\247MMA\310\301Z<\366\246\023\0161W3\340\357\000\317T\t\301\311+\204\246L7\276\370\320*\245O\021\002\000k\024\221\270x\353\001\237\346D}\377?\265]\356+\243\v[\350\316a\340h\376<\322\266\327\016\306n\272r\t\212A\253L\216\214\205\016\241 [\360/\335\002#\\A\372\241a\261\346\346\\\251\330\312\365\016\n\341\017\016\225&;\322\\\004*\ru\316\372\a \362?8\031\247\231\030\030\267\315\023\v\343{@\227\301s\372h\212\000a\244&\231\366\nt\277\2036,\027bZ+\223W\212g\333`\264\331N\306\307\362\257(^~ b\262\247&\231\261t\341\231%\244\247\203eOt\365\271\201\273\330\350\363C^A\327F\214!\217hgf\e\320k\260n\315u~\336\371M\t\235k\230S\375\311\303\240\351\037d\273\321y\335=K\016`_\317\230\2612_\023K\036\350\v\232\323Y\310\317_\035\227%\237\v\340\023\016\243\233\025\306:\227\351\370\364x\234\231\266\367\016w\275\333-\351\210}\375x\034\262\272kRuHa\362T/F!\347B\231O`K\304\037'k$$\245h)e\363\365mT\b\317\\2\361\026\351\254\375Jl1~\r\371\267\352\2322I\341\272\376\243^Un\266E7\230[VocUJ\220N\2116D/\025f=\213\314\325\vG}\311\360\377DT\307m\261&\263\340\272\243_\020\271rG^BW\210\030l\344\0324\335\233\300\023\272\225Im\330\n\227*Yv[\006\315\330y'\a\321\373\273A\240\305F{S\246I#/\355\2425\031\031GGF\270y\n\331\004\023G@\331\000\361\343\350\264$\032\355_\210y\000\205\342\375\212q\024\004\026W:\205 \363v?\035\270L-\270=\022\323\2003\v\336\277\t\237\356\374\n\267n\003\367\342\330;\371S\326\016`B6@Njm>\240\021%\336\345\002(P\204Yn\3279l\0228\264\254\304\2528t\372h\217\347sA\314\345\245\337)]\000\b\000\021\000\032\000\035\000+\0007\000Z\000m\000\264\000\000\000\000\000\000\002\001\000\000\000\000\000\000\000\t\000\000\000\000\000\000\000\000\000\000\000\000\000\000\002\270" } # The below is a base64 encoded salted-SHA512 password hash. let (:pw_string) { "\335\006{\3744g@\377z\204\322\r\332t\021\330\n\003\246K\223\356\034!P\261\305t\035\346\352p\206\003n\247MMA\310\301Z<\366\246\023\0161W3\340\357\000\317T\t\301\311+\204\246L7\276\370\320*\245" } # The below is a salted-SHA512 password hash in hex. let (:sha512_hash) { 'dd067bfc346740ff7a84d20dda7411d80a03a64b93ee1c2150b1c5741de6ea7086036ea74d4d41c8c15a3cf6a6130e315733e0ef00cf5409c1c92b84a64c37bef8d02aa5' } let :plist_path do '/var/db/dslocal/nodes/Default/users/jeff.plist' end let :ds_provider do Puppet::Provider::NameService::DirectoryService end let :shadow_hash_data do {'ShadowHashData' => [StringIO.new(binary_plist)]} end subject do Puppet::Provider::NameService::DirectoryService end before :each do subject.expects(:get_macosx_version_major).returns("10.7") end it 'should execute convert_binary_to_xml once when getting the password on >= 10.7' do subject.expects(:convert_binary_to_xml).returns({'SALTED-SHA512' => StringIO.new(pw_string)}) File.expects(:exists?).with(plist_path).once.returns(true) Plist.expects(:parse_xml).returns(shadow_hash_data) # On Mac OS X 10.7 we first need to convert to xml when reading the password subject.expects(:plutil).with('-convert', 'xml1', '-o', '/dev/stdout', plist_path) subject.get_password('uid', 'jeff') end it 'should fail if a salted-SHA512 password hash is not passed in >= 10.7' do expect { subject.set_password('jeff', 'uid', 'badpassword') }.should raise_error(RuntimeError, /OS X 10.7 requires a Salted SHA512 hash password of 136 characters./) end it 'should convert xml-to-binary and binary-to-xml when setting the pw on >= 10.7' do subject.expects(:convert_binary_to_xml).returns({'SALTED-SHA512' => StringIO.new(pw_string)}) subject.expects(:convert_xml_to_binary).returns(binary_plist) File.expects(:exists?).with(plist_path).once.returns(true) Plist.expects(:parse_xml).returns(shadow_hash_data) # On Mac OS X 10.7 we first need to convert to xml subject.expects(:plutil).with('-convert', 'xml1', '-o', '/dev/stdout', plist_path) # And again back to a binary plist or DirectoryService will complain subject.expects(:plutil).with('-convert', 'binary1', plist_path) Plist::Emit.expects(:save_plist).with(shadow_hash_data, plist_path) subject.set_password('jeff', 'uid', sha512_hash) end + + it '[#13686] should handle an empty ShadowHashData field in the users plist' do + subject.expects(:convert_xml_to_binary).returns(binary_plist) + File.expects(:exists?).with(plist_path).once.returns(true) + Plist.expects(:parse_xml).returns({'ShadowHashData' => nil}) + subject.expects(:plutil).with('-convert', 'xml1', '-o', '/dev/stdout', plist_path) + subject.expects(:plutil).with('-convert', 'binary1', plist_path) + Plist::Emit.expects(:save_plist) + subject.set_password('jeff', 'uid', sha512_hash) + end end describe '(#4855) directoryservice group resource failure' do let :provider_class do Puppet::Type.type(:group).provider(:directoryservice) end let :group_members do ['root','jeff'] end let :user_account do ['root'] end let :stub_resource do stub('resource') end subject do provider_class.new(stub_resource) end before :each do @resource = stub("resource") @provider = provider_class.new(@resource) end it 'should delete a group member if the user does not exist' do stub_resource.stubs(:[]).with(:name).returns('fake_group') stub_resource.stubs(:name).returns('fake_group') subject.expects(:execute).with([:dseditgroup, '-o', 'edit', '-n', '.', '-d', 'jeff', 'fake_group']).raises(Puppet::ExecutionFailure, 'it broke') subject.expects(:execute).with([:dscl, '.', '-delete', '/Groups/fake_group', 'GroupMembership', 'jeff']) subject.remove_unwanted_members(group_members, user_account) end end diff --git a/spec/unit/reports/tagmail_spec.rb b/spec/unit/reports/tagmail_spec.rb index a53d11978..00f78c932 100755 --- a/spec/unit/reports/tagmail_spec.rb +++ b/spec/unit/reports/tagmail_spec.rb @@ -1,91 +1,168 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/reports' tagmail = Puppet::Reports.report(:tagmail) describe tagmail do before do @processor = Puppet::Transaction::Report.new("apply") @processor.extend(Puppet::Reports.report(:tagmail)) end passers = my_fixture "tagmail_passers.conf" File.readlines(passers).each do |line| it "should be able to parse '#{line.inspect}'" do @processor.parse(line) end end failers = my_fixture "tagmail_failers.conf" File.readlines(failers).each do |line| it "should not be able to parse '#{line.inspect}'" do lambda { @processor.parse(line) }.should raise_error(ArgumentError) end end { "tag: abuse@domain.com" => [%w{abuse@domain.com}, %w{tag}, []], "tag.localhost: abuse@domain.com" => [%w{abuse@domain.com}, %w{tag.localhost}, []], "tag, other: abuse@domain.com" => [%w{abuse@domain.com}, %w{tag other}, []], "tag-other: abuse@domain.com" => [%w{abuse@domain.com}, %w{tag-other}, []], "tag, !other: abuse@domain.com" => [%w{abuse@domain.com}, %w{tag}, %w{other}], "tag, !other, one, !two: abuse@domain.com" => [%w{abuse@domain.com}, %w{tag one}, %w{other two}], "tag: abuse@domain.com, other@domain.com" => [%w{abuse@domain.com other@domain.com}, %w{tag}, []] }.each do |line, results| it "should parse '#{line}' as #{results.inspect}" do @processor.parse(line).shift.should == results end end describe "when matching logs" do before do @processor << Puppet::Util::Log.new(:level => :notice, :message => "first", :tags => %w{one}) @processor << Puppet::Util::Log.new(:level => :notice, :message => "second", :tags => %w{one two}) @processor << Puppet::Util::Log.new(:level => :notice, :message => "third", :tags => %w{one two three}) end def match(pos = [], neg = []) pos = Array(pos) neg = Array(neg) result = @processor.match([[%w{abuse@domain.com}, pos, neg]]) actual_result = result.shift if actual_result actual_result[1] else nil end end it "should match all messages when provided the 'all' tag as a positive matcher" do results = match("all") %w{first second third}.each do |str| results.should be_include(str) end end it "should remove messages that match a negated tag" do match("all", "three").should_not be_include("third") end it "should find any messages tagged with a provided tag" do results = match("two") results.should be_include("second") results.should be_include("third") results.should_not be_include("first") end it "should allow negation of specific tags from a specific tag list" do results = match("two", "three") results.should be_include("second") results.should_not be_include("third") end it "should allow a tag to negate all matches" do results = match([], "one") results.should be_nil end end + + describe "the behavior of tagmail.process" do + before do + Puppet[:tagmap] = my_fixture "tagmail_email.conf" + end + + let(:processor) do + processor = Puppet::Transaction::Report.new("apply") + processor.extend(Puppet::Reports.report(:tagmail)) + processor + end + + context "when any messages match a positive tag" do + before do + processor << log_entry + end + + let(:log_entry) do + Puppet::Util::Log.new( + :level => :notice, :message => "Secure change", :tags => %w{secure}) + end + + let(:message) do + "#{log_entry.time} Puppet (notice): Secure change" + end + + it "should send email if there are changes" do + processor.expects(:send).with([[['user@domain.com'], message]]) + processor.expects(:raw_summary).returns({ + "resources" => { "changed" => 1, "out_of_sync" => 0 } + }) + + processor.process + end + + it "should send email if there are resources out of sync" do + processor.expects(:send).with([[['user@domain.com'], message]]) + processor.expects(:raw_summary).returns({ + "resources" => { "changed" => 0, "out_of_sync" => 1 } + }) + + processor.process + end + + it "should not send email if no changes or resources out of sync" do + processor.expects(:send).never + processor.expects(:raw_summary).returns({ + "resources" => { "changed" => 0, "out_of_sync" => 0 } + }) + + processor.process + end + + it "should log a message if no changes or resources out of sync" do + processor.expects(:send).never + processor.expects(:raw_summary).returns({ + "resources" => { "changed" => 0, "out_of_sync" => 0 } + }) + + Puppet.expects(:notice).with("Not sending tagmail report; no changes") + processor.process + end + + it "should send email if raw_summary is not defined" do + processor.expects(:send).with([[['user@domain.com'], message]]) + processor.expects(:raw_summary).returns(nil) + processor.process + end + + it "should send email if there are no resource metrics" do + processor.expects(:send).with([[['user@domain.com'], message]]) + processor.expects(:raw_summary).returns({'resources' => nil}) + processor.process + end + 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/type/user_spec.rb b/spec/unit/type/user_spec.rb index c559be3c7..bb235b798 100755 --- a/spec/unit/type/user_spec.rb +++ b/spec/unit/type/user_spec.rb @@ -1,335 +1,340 @@ #!/usr/bin/env rspec require 'spec_helper' -user = Puppet::Type.type(:user) - -describe user do - before do - @provider = stub 'provider' - @resource = stub 'resource', :resource => nil, :provider => @provider, :line => nil, :file => nil +describe Puppet::Type.type(:user) do + before :each do + @provider_class = described_class.provide(:simple) do + has_features :manages_expiry, :manages_password_age, :manages_passwords, :manages_solaris_rbac + mk_resource_methods + def create; end + def delete; end + def exists?; get(:ensure) != :absent; end + def flush; end + def self.instances; []; end + end + described_class.stubs(:defaultprovider).returns @provider_class end it "should be able to create a instance" do - user.new(:name => "foo").should_not be_nil + described_class.new(:name => "foo").should_not be_nil end it "should have an allows_duplicates feature" do - user.provider_feature(:allows_duplicates).should_not be_nil + described_class.provider_feature(:allows_duplicates).should_not be_nil end it "should have an manages_homedir feature" do - user.provider_feature(:manages_homedir).should_not be_nil + described_class.provider_feature(:manages_homedir).should_not be_nil end it "should have an manages_passwords feature" do - user.provider_feature(:manages_passwords).should_not be_nil + described_class.provider_feature(:manages_passwords).should_not be_nil end it "should have a manages_solaris_rbac feature" do - user.provider_feature(:manages_solaris_rbac).should_not be_nil + described_class.provider_feature(:manages_solaris_rbac).should_not be_nil end it "should have a manages_expiry feature" do - user.provider_feature(:manages_expiry).should_not be_nil + described_class.provider_feature(:manages_expiry).should_not be_nil end it "should have a manages_password_age feature" do - user.provider_feature(:manages_password_age).should_not be_nil + described_class.provider_feature(:manages_password_age).should_not be_nil end it "should have a system_users feature" do - user.provider_feature(:system_users).should_not be_nil + described_class.provider_feature(:system_users).should_not be_nil end describe "instances" do it "should delegate existence questions to its provider" do - instance = user.new(:name => "foo") - instance.provider.expects(:exists?).returns "eh" - instance.exists?.should == "eh" + @provider = @provider_class.new(:name => 'foo', :ensure => :absent) + instance = described_class.new(:name => "foo", :provider => @provider) + instance.exists?.should == false + + @provider.set(:ensure => :present) + instance.exists?.should == true end end properties = [:ensure, :uid, :gid, :home, :comment, :shell, :password, :password_min_age, :password_max_age, :groups, :roles, :auths, :profiles, :project, :keys, :expiry] properties.each do |property| it "should have a #{property} property" do - user.attrclass(property).ancestors.should be_include(Puppet::Property) + described_class.attrclass(property).ancestors.should be_include(Puppet::Property) end it "should have documentation for its #{property} property" do - user.attrclass(property).doc.should be_instance_of(String) + described_class.attrclass(property).doc.should be_instance_of(String) end end list_properties = [:groups, :roles, :auths] list_properties.each do |property| it "should have a list '#{property}'" do - user.attrclass(property).ancestors.should be_include(Puppet::Property::List) + described_class.attrclass(property).ancestors.should be_include(Puppet::Property::List) end end it "should have an ordered list 'profiles'" do - user.attrclass(:profiles).ancestors.should be_include(Puppet::Property::OrderedList) + described_class.attrclass(:profiles).ancestors.should be_include(Puppet::Property::OrderedList) end it "should have key values 'keys'" do - user.attrclass(:keys).ancestors.should be_include(Puppet::Property::KeyValue) + described_class.attrclass(:keys).ancestors.should be_include(Puppet::Property::KeyValue) end describe "when retrieving all current values" do before do - @user = user.new(:name => "foo", :uid => 10) + @provider = @provider_class.new(:name => 'foo', :ensure => :present, :uid => 15, :gid => 15) + @user = described_class.new(:name => "foo", :uid => 10, :provider => @provider) end it "should return a hash containing values for all set properties" do @user[:gid] = 10 - @user.property(:ensure).expects(:retrieve).returns :present - @user.property(:uid).expects(:retrieve).returns 15 - @user.property(:gid).expects(:retrieve).returns 15 values = @user.retrieve [@user.property(:uid), @user.property(:gid)].each { |property| values.should be_include(property) } end it "should set all values to :absent if the user is absent" do @user.property(:ensure).expects(:retrieve).returns :absent @user.property(:uid).expects(:retrieve).never @user.retrieve[@user.property(:uid)].should == :absent end it "should include the result of retrieving each property's current value if the user is present" do - @user.property(:ensure).expects(:retrieve).returns :present - @user.property(:uid).expects(:retrieve).returns 15 @user.retrieve[@user.property(:uid)].should == 15 end end describe "when managing the ensure property" do - before do - @ensure = user.attrclass(:ensure).new(:resource => @resource) - end - it "should support a :present value" do - lambda { @ensure.should = :present }.should_not raise_error + lambda { described_class.new(:name => 'foo', :ensure => :present) }.should_not raise_error end it "should support an :absent value" do - lambda { @ensure.should = :absent }.should_not raise_error + lambda { described_class.new(:name => 'foo', :ensure => :absent) }.should_not raise_error end it "should call :create on the provider when asked to sync to the :present state" do + @provider = @provider_class.new(:name => 'foo', :ensure => :absent) @provider.expects(:create) - @ensure.should = :present - @ensure.sync + described_class.new(:name => 'foo', :ensure => :present, :provider => @provider).parameter(:ensure).sync end it "should call :delete on the provider when asked to sync to the :absent state" do + @provider = @provider_class.new(:name => 'foo', :ensure => :present) @provider.expects(:delete) - @ensure.should = :absent - @ensure.sync + described_class.new(:name => 'foo', :ensure => :absent, :provider => @provider).parameter(:ensure).sync end describe "and determining the current state" do it "should return :present when the provider indicates the user exists" do - @provider.expects(:exists?).returns true - @ensure.retrieve.should == :present + @provider = @provider_class.new(:name => 'foo', :ensure => :present) + described_class.new(:name => 'foo', :ensure => :absent, :provider => @provider).parameter(:ensure).retrieve.should == :present end it "should return :absent when the provider indicates the user does not exist" do - @provider.expects(:exists?).returns false - @ensure.retrieve.should == :absent + @provider = @provider_class.new(:name => 'foo', :ensure => :absent) + described_class.new(:name => 'foo', :ensure => :present, :provider => @provider).parameter(:ensure).retrieve.should == :absent end end end describe "when managing the uid property" do it "should convert number-looking strings into actual numbers" do - uid = user.attrclass(:uid).new(:resource => @resource) - uid.should = "50" - uid.should.must == 50 + described_class.new(:name => 'foo', :uid => '50')[:uid].should == 50 end it "should support UIDs as numbers" do - uid = user.attrclass(:uid).new(:resource => @resource) - uid.should = 50 - uid.should.must == 50 + described_class.new(:name => 'foo', :uid => 50)[:uid].should == 50 end - it "should :absent as a value" do - uid = user.attrclass(:uid).new(:resource => @resource) - uid.should = :absent - uid.should.must == :absent + it "should support :absent as a value" do + described_class.new(:name => 'foo', :uid => :absent)[:uid].should == :absent end end describe "when managing the gid" do - it "should :absent as a value" do - gid = user.attrclass(:gid).new(:resource => @resource) - gid.should = :absent - gid.should.must == :absent + it "should support :absent as a value" do + described_class.new(:name => 'foo', :gid => :absent)[:gid].should == :absent end it "should convert number-looking strings into actual numbers" do - gid = user.attrclass(:gid).new(:resource => @resource) - gid.should = "50" - gid.should.must == 50 + described_class.new(:name => 'foo', :gid => '50')[:gid].should == 50 end it "should support GIDs specified as integers" do - gid = user.attrclass(:gid).new(:resource => @resource) - gid.should = 50 - gid.should.must == 50 + described_class.new(:name => 'foo', :gid => 50)[:gid].should == 50 end it "should support groups specified by name" do - gid = user.attrclass(:gid).new(:resource => @resource) - gid.should = "foo" - gid.should.must == "foo" + described_class.new(:name => 'foo', :gid => 'foo')[:gid].should == 'foo' end describe "when testing whether in sync" do - before do - @gid = user.attrclass(:gid).new(:resource => @resource, :should => %w{foo bar}) - end - it "should return true if no 'should' values are set" do - @gid = user.attrclass(:gid).new(:resource => @resource) - - @gid.must be_safe_insync(500) + # this is currently not the case because gid has no default value, so we would never even + # call insync? on that property + if param = described_class.new(:name => 'foo').parameter(:gid) + param.must be_safe_insync(500) + end end it "should return true if any of the specified groups are equal to the current integer" do Puppet::Util.expects(:gid).with("foo").returns 300 Puppet::Util.expects(:gid).with("bar").returns 500 - - @gid.must be_safe_insync(500) + described_class.new(:name => 'baz', :gid => [ 'foo', 'bar' ]).parameter(:gid).must be_safe_insync(500) end it "should return false if none of the specified groups are equal to the current integer" do Puppet::Util.expects(:gid).with("foo").returns 300 Puppet::Util.expects(:gid).with("bar").returns 500 - - @gid.should_not be_safe_insync(700) + described_class.new(:name => 'baz', :gid => [ 'foo', 'bar' ]).parameter(:gid).must_not be_safe_insync(700) end end describe "when syncing" do - before do - @gid = user.attrclass(:gid).new(:resource => @resource, :should => %w{foo bar}) - end - it "should use the first found, specified group as the desired value and send it to the provider" do Puppet::Util.expects(:gid).with("foo").returns nil Puppet::Util.expects(:gid).with("bar").returns 500 - @provider.expects(:gid=).with 500 + @provider = @provider_class.new(:name => 'foo') + resource = described_class.new(:name => 'foo', :provider => @provider, :gid => [ 'foo', 'bar' ]) - @gid.sync + @provider.expects(:gid=).with 500 + resource.parameter(:gid).sync end end end - describe "when managing expiry" do - before do - @expiry = user.attrclass(:expiry).new(:resource => @resource) + describe "when managing groups" do + it "should support a singe group" do + lambda { described_class.new(:name => 'foo', :groups => 'bar') }.should_not raise_error end - it "should fail if given an invalid date" do - lambda { @expiry.should = "200-20-20" }.should raise_error(Puppet::Error) + it "should support multiple groups as an array" do + lambda { described_class.new(:name => 'foo', :groups => [ 'bar' ]) }.should_not raise_error + lambda { described_class.new(:name => 'foo', :groups => [ 'bar', 'baz' ]) }.should_not raise_error + end + + it "should not support a comma separated list" do + lambda { described_class.new(:name => 'foo', :groups => 'bar,baz') }.should raise_error(Puppet::Error, /Group names must be provided as an array/) + end + + it "should not support an empty string" do + lambda { described_class.new(:name => 'foo', :groups => '') }.should raise_error(Puppet::Error, /Group names must not be empty/) + end + + describe "when testing is in sync" do + + before :each do + # the useradd provider uses a single string to represent groups and so does Puppet::Property::List when converting to should values + @provider = @provider_class.new(:name => 'foo', :groups => 'a,b,e,f') + end + + it "should not care about order" do + @property = described_class.new(:name => 'foo', :groups => [ 'a', 'c', 'b' ]).property(:groups) + @property.must be_safe_insync([ 'a', 'b', 'c' ]) + @property.must be_safe_insync([ 'a', 'c', 'b' ]) + @property.must be_safe_insync([ 'b', 'a', 'c' ]) + @property.must be_safe_insync([ 'b', 'c', 'a' ]) + @property.must be_safe_insync([ 'c', 'a', 'b' ]) + @property.must be_safe_insync([ 'c', 'b', 'a' ]) + end + + it "should merge current value and desired value if membership minimal" do + @instance = described_class.new(:name => 'foo', :groups => [ 'a', 'c', 'b' ], :provider => @provider) + @instance[:membership] = :minimum + @instance[:groups].should == 'a,b,c,e,f' + end + + it "should not treat a subset of groups insync if membership inclusive" do + @instance = described_class.new(:name => 'foo', :groups => [ 'a', 'c', 'b' ], :provider => @provider) + @instance[:membership] = :inclusive + @instance[:groups].should == 'a,b,c' + end end end - describe "when managing minimum password age" do - before do - @age = user.attrclass(:password_min_age).new(:resource => @resource) + + describe "when managing expiry" do + it "should fail if given an invalid date" do + lambda { described_class.new(:name => 'foo', :expiry => "200-20-20") }.should raise_error(Puppet::Error, /Expiry dates must be YYYY-MM-DD/) end + end + describe "when managing minimum password age" do it "should accept a negative minimum age" do - expect { @age.should = -1 }.should_not raise_error + expect { described_class.new(:name => 'foo', :password_min_age => '-1') }.should_not raise_error end it "should fail with an empty minimum age" do - expect { @age.should = '' }.should raise_error(Puppet::Error) + expect { described_class.new(:name => 'foo', :password_min_age => '') }.should raise_error(Puppet::Error, /minimum age must be provided as a number/) end end describe "when managing maximum password age" do - before do - @age = user.attrclass(:password_max_age).new(:resource => @resource) - end - it "should accept a negative maximum age" do - expect { @age.should = -1 }.should_not raise_error + expect { described_class.new(:name => 'foo', :password_max_age => '-1') }.should_not raise_error end it "should fail with an empty maximum age" do - expect { @age.should = '' }.should raise_error(Puppet::Error) + expect { described_class.new(:name => 'foo', :password_max_age => '') }.should raise_error(Puppet::Error, /maximum age must be provided as a number/) end end describe "when managing passwords" do before do - @password = user.attrclass(:password).new(:resource => @resource, :should => "mypass") + @password = described_class.new(:name => 'foo', :password => 'mypass').parameter(:password) end it "should not include the password in the change log when adding the password" do @password.change_to_s(:absent, "mypass").should_not be_include("mypass") end it "should not include the password in the change log when changing the password" do @password.change_to_s("other", "mypass").should_not be_include("mypass") end it "should redact the password when displaying the old value" do @password.is_to_s("currentpassword").should =~ /^\[old password hash redacted\]$/ end it "should redact the password when displaying the new value" do @password.should_to_s("newpassword").should =~ /^\[new password hash redacted\]$/ end it "should fail if a ':' is included in the password" do - lambda { @password.should = "some:thing" }.should raise_error(Puppet::Error) + lambda { described_class.new(:name => 'foo', :password => "some:thing") }.should raise_error(Puppet::Error, /Passwords cannot include ':'/) end it "should allow the value to be set to :absent" do - lambda { @password.should = :absent }.should_not raise_error + lambda { described_class.new(:name => 'foo', :password => :absent) }.should_not raise_error end end describe "when manages_solaris_rbac is enabled" do - before do - @provider.stubs(:satisfies?).returns(false) - @provider.expects(:satisfies?).with([:manages_solaris_rbac]).returns(true) - end - it "should support a :role value for ensure" do - @ensure = user.attrclass(:ensure).new(:resource => @resource) - lambda { @ensure.should = :role }.should_not raise_error + lambda { described_class.new(:name => 'foo', :ensure => :role) }.should_not raise_error end end describe "when user has roles" do - before do - # To test this feature, we have to support it. - user.new(:name => "foo").provider.class.stubs(:feature?).returns(true) - end - it "should autorequire roles" do - testuser = Puppet::Type.type(:user).new(:name => "testuser") - testuser.provider.stubs(:send).with(:roles).returns("") - testuser[:roles] = "testrole" - - testrole = Puppet::Type.type(:user).new(:name => "testrole") + testuser = described_class.new(:name => "testuser", :roles => ['testrole'] ) + testrole = described_class.new(:name => "testrole") config = Puppet::Resource::Catalog.new :testing do |conf| [testuser, testrole].each { |resource| conf.add_resource resource } end Puppet::Type::User::ProviderDirectoryservice.stubs(:get_macosx_version_major).returns "10.5" rel = testuser.autorequire[0] rel.source.ref.should == testrole.ref rel.target.ref.should == testuser.ref 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 diff --git a/spec/unit/util_spec.rb b/spec/unit/util_spec.rb index 704e2851c..40df8c3bf 100755 --- a/spec/unit/util_spec.rb +++ b/spec/unit/util_spec.rb @@ -1,435 +1,455 @@ #!/usr/bin/env ruby require 'spec_helper' describe Puppet::Util do include PuppetSpec::Files if Puppet.features.microsoft_windows? def set_mode(mode, file) Puppet::Util::Windows::Security.set_mode(mode, file) end def get_mode(file) Puppet::Util::Windows::Security.get_mode(file) & 07777 end else def set_mode(mode, file) File.chmod(mode, file) end def get_mode(file) File.lstat(file).mode & 07777 end end describe "#withenv" do before :each do @original_path = ENV["PATH"] @new_env = {:PATH => "/some/bogus/path"} end it "should change environment variables within the block then reset environment variables to their original values" do Puppet::Util.withenv @new_env do ENV["PATH"].should == "/some/bogus/path" end ENV["PATH"].should == @original_path end it "should reset environment variables to their original values even if the block fails" do begin Puppet::Util.withenv @new_env do ENV["PATH"].should == "/some/bogus/path" raise "This is a failure" end rescue end ENV["PATH"].should == @original_path end it "should reset environment variables even when they are set twice" do # Setting Path & Environment parameters in Exec type can cause weirdness @new_env["PATH"] = "/someother/bogus/path" Puppet::Util.withenv @new_env do # When assigning duplicate keys, can't guarantee order of evaluation ENV["PATH"].should =~ /\/some.*\/bogus\/path/ end ENV["PATH"].should == @original_path end it "should remove any new environment variables after the block ends" do @new_env[:FOO] = "bar" Puppet::Util.withenv @new_env do ENV["FOO"].should == "bar" end ENV["FOO"].should == nil end end describe "#absolute_path?" do it "should default to the platform of the local system" do Puppet.features.stubs(:posix?).returns(true) Puppet.features.stubs(:microsoft_windows?).returns(false) Puppet::Util.should be_absolute_path('/foo') Puppet::Util.should_not be_absolute_path('C:/foo') Puppet.features.stubs(:posix?).returns(false) Puppet.features.stubs(:microsoft_windows?).returns(true) Puppet::Util.should be_absolute_path('C:/foo') Puppet::Util.should_not be_absolute_path('/foo') end describe "when using platform :posix" do %w[/ /foo /foo/../bar //foo //Server/Foo/Bar //?/C:/foo/bar /\Server/Foo /foo//bar/baz].each do |path| it "should return true for #{path}" do Puppet::Util.should be_absolute_path(path, :posix) end end %w[. ./foo \foo C:/foo \\Server\Foo\Bar \\?\C:\foo\bar \/?/foo\bar \/Server/foo foo//bar/baz].each do |path| it "should return false for #{path}" do Puppet::Util.should_not be_absolute_path(path, :posix) end end end describe "when using platform :windows" do %w[C:/foo C:\foo \\\\Server\Foo\Bar \\\\?\C:\foo\bar //Server/Foo/Bar //?/C:/foo/bar /\?\C:/foo\bar \/Server\Foo/Bar c:/foo//bar//baz].each do |path| it "should return true for #{path}" do Puppet::Util.should be_absolute_path(path, :windows) end end %w[/ . ./foo \foo /foo /foo/../bar //foo C:foo/bar foo//bar/baz].each do |path| it "should return false for #{path}" do Puppet::Util.should_not be_absolute_path(path, :windows) end end end end describe "#path_to_uri" do %w[. .. foo foo/bar foo/../bar].each do |path| it "should reject relative path: #{path}" do lambda { Puppet::Util.path_to_uri(path) }.should raise_error(Puppet::Error) end end it "should perform URI escaping" do Puppet::Util.path_to_uri("/foo bar").path.should == "/foo%20bar" end describe "when using platform :posix" do before :each do Puppet.features.stubs(:posix).returns true Puppet.features.stubs(:microsoft_windows?).returns false end %w[/ /foo /foo/../bar].each do |path| it "should convert #{path} to URI" do Puppet::Util.path_to_uri(path).path.should == path end end end describe "when using platform :windows" do before :each do Puppet.features.stubs(:posix).returns false Puppet.features.stubs(:microsoft_windows?).returns true end it "should normalize backslashes" do Puppet::Util.path_to_uri('c:\\foo\\bar\\baz').path.should == '/' + 'c:/foo/bar/baz' end %w[C:/ C:/foo/bar].each do |path| it "should convert #{path} to absolute URI" do Puppet::Util.path_to_uri(path).path.should == '/' + path end end %w[share C$].each do |path| it "should convert UNC #{path} to absolute URI" do uri = Puppet::Util.path_to_uri("\\\\server\\#{path}") uri.host.should == 'server' uri.path.should == '/' + path end end end end describe ".uri_to_path" do require 'uri' it "should strip host component" do Puppet::Util.uri_to_path(URI.parse('http://foo/bar')).should == '/bar' end it "should accept puppet URLs" do Puppet::Util.uri_to_path(URI.parse('puppet:///modules/foo')).should == '/modules/foo' end it "should return unencoded path" do Puppet::Util.uri_to_path(URI.parse('http://foo/bar%20baz')).should == '/bar baz' end it "should be nil-safe" do Puppet::Util.uri_to_path(nil).should be_nil end describe "when using platform :posix",:if => Puppet.features.posix? do it "should accept root" do Puppet::Util.uri_to_path(URI.parse('file:/')).should == '/' end it "should accept single slash" do Puppet::Util.uri_to_path(URI.parse('file:/foo/bar')).should == '/foo/bar' end it "should accept triple slashes" do Puppet::Util.uri_to_path(URI.parse('file:///foo/bar')).should == '/foo/bar' end end describe "when using platform :windows", :if => Puppet.features.microsoft_windows? do it "should accept root" do Puppet::Util.uri_to_path(URI.parse('file:/C:/')).should == 'C:/' end it "should accept single slash" do Puppet::Util.uri_to_path(URI.parse('file:/C:/foo/bar')).should == 'C:/foo/bar' end it "should accept triple slashes" do Puppet::Util.uri_to_path(URI.parse('file:///C:/foo/bar')).should == 'C:/foo/bar' end it "should accept file scheme with double slashes as a UNC path" do Puppet::Util.uri_to_path(URI.parse('file://host/share/file')).should == '//host/share/file' end end end describe "#which" do let(:base) { File.expand_path('/bin') } let(:path) { File.join(base, 'foo') } before :each do FileTest.stubs(:file?).returns false FileTest.stubs(:file?).with(path).returns true FileTest.stubs(:executable?).returns false FileTest.stubs(:executable?).with(path).returns true end it "should accept absolute paths" do Puppet::Util.which(path).should == path end it "should return nil if no executable found" do Puppet::Util.which('doesnotexist').should be_nil end it "should warn if the user's HOME is not set but their PATH contains a ~" do env_path = %w[~/bin /usr/bin /bin].join(File::PATH_SEPARATOR) Puppet::Util.withenv({:HOME => nil, :PATH => env_path}) do Puppet::Util::Warnings.expects(:warnonce).once Puppet::Util.which('foo') end end it "should reject directories" do Puppet::Util.which(base).should be_nil end describe "on POSIX systems" do before :each do Puppet.features.stubs(:posix?).returns true Puppet.features.stubs(:microsoft_windows?).returns false end it "should walk the search PATH returning the first executable" do ENV.stubs(:[]).with('PATH').returns(File.expand_path('/bin')) Puppet::Util.which('foo').should == path end end describe "on Windows systems" do let(:path) { File.expand_path(File.join(base, 'foo.CMD')) } before :each do Puppet.features.stubs(:posix?).returns false Puppet.features.stubs(:microsoft_windows?).returns true end describe "when a file extension is specified" do it "should walk each directory in PATH ignoring PATHEXT" do ENV.stubs(:[]).with('PATH').returns(%w[/bar /bin].map{|dir| File.expand_path(dir)}.join(File::PATH_SEPARATOR)) FileTest.expects(:file?).with(File.join(File.expand_path('/bar'), 'foo.CMD')).returns false ENV.expects(:[]).with('PATHEXT').never Puppet::Util.which('foo.CMD').should == path end end describe "when a file extension is not specified" do it "should walk each extension in PATHEXT until an executable is found" do bar = File.expand_path('/bar') ENV.stubs(:[]).with('PATH').returns("#{bar}#{File::PATH_SEPARATOR}#{base}") ENV.stubs(:[]).with('PATHEXT').returns(".EXE#{File::PATH_SEPARATOR}.CMD") exts = sequence('extensions') FileTest.expects(:file?).in_sequence(exts).with(File.join(bar, 'foo.EXE')).returns false FileTest.expects(:file?).in_sequence(exts).with(File.join(bar, 'foo.CMD')).returns false FileTest.expects(:file?).in_sequence(exts).with(File.join(base, 'foo.EXE')).returns false FileTest.expects(:file?).in_sequence(exts).with(path).returns true Puppet::Util.which('foo').should == path end it "should walk the default extension path if the environment variable is not defined" do ENV.stubs(:[]).with('PATH').returns(base) ENV.stubs(:[]).with('PATHEXT').returns(nil) exts = sequence('extensions') %w[.COM .EXE .BAT].each do |ext| FileTest.expects(:file?).in_sequence(exts).with(File.join(base, "foo#{ext}")).returns false end FileTest.expects(:file?).in_sequence(exts).with(path).returns true Puppet::Util.which('foo').should == path end it "should fall back if no extension matches" do ENV.stubs(:[]).with('PATH').returns(base) ENV.stubs(:[]).with('PATHEXT').returns(".EXE") FileTest.stubs(:file?).with(File.join(base, 'foo.EXE')).returns false FileTest.stubs(:file?).with(File.join(base, 'foo')).returns true FileTest.stubs(:executable?).with(File.join(base, 'foo')).returns true Puppet::Util.which('foo').should == File.join(base, 'foo') end end end end describe "#binread" do let(:contents) { "foo\r\nbar" } it "should preserve line endings" do path = tmpfile('util_binread') File.open(path, 'wb') { |f| f.print contents } Puppet::Util.binread(path).should == contents end it "should raise an error if the file doesn't exist" do expect { Puppet::Util.binread('/path/does/not/exist') }.to raise_error(Errno::ENOENT) end end + describe "hash symbolizing functions" do + let (:myhash) { { "foo" => "bar", :baz => "bam" } } + let (:resulthash) { { :foo => "bar", :baz => "bam" } } + + describe "#symbolizehash" do + it "should return a symbolized hash" do + newhash = Puppet::Util.symbolizehash(myhash) + newhash.should == resulthash + end + end + + describe "#symbolizehash!" do + it "should symbolize the hash in place" do + localhash = myhash + Puppet::Util.symbolizehash!(localhash) + localhash.should == resulthash + end + end + end + context "#replace_file" do subject { Puppet::Util } it { should respond_to :replace_file } let :target do target = Tempfile.new("puppet-util-replace-file") target.puts("hello, world") target.flush # make sure content is on disk. target.fsync rescue nil target.close target end it "should fail if no block is given" do expect { subject.replace_file(target.path, 0600) }.to raise_error /block/ end it "should replace a file when invoked" do # Check that our file has the expected content. File.read(target.path).should == "hello, world\n" # Replace the file. subject.replace_file(target.path, 0600) do |fh| fh.puts "I am the passenger..." end # ...and check the replacement was complete. File.read(target.path).should == "I am the passenger...\n" end [0555, 0600, 0660, 0700, 0770].each do |mode| it "should copy 0#{mode.to_s(8)} permissions from the target file by default" do set_mode(mode, target.path) get_mode(target.path).should == mode subject.replace_file(target.path, 0000) {|fh| fh.puts "bazam" } get_mode(target.path).should == mode File.read(target.path).should == "bazam\n" end end it "should copy the permissions of the source file before yielding" do set_mode(0555, target.path) inode = File.stat(target.path).ino unless Puppet.features.microsoft_windows? yielded = false subject.replace_file(target.path, 0600) do |fh| get_mode(fh.path).should == 0555 yielded = true end yielded.should be_true # We can't check inode on Windows File.stat(target.path).ino.should_not == inode unless Puppet.features.microsoft_windows? get_mode(target.path).should == 0555 end it "should use the default permissions if the source file doesn't exist" do new_target = target.path + '.foo' File.should_not be_exist(new_target) begin subject.replace_file(new_target, 0555) {|fh| fh.puts "foo" } get_mode(new_target).should == 0555 ensure File.unlink(new_target) if File.exists?(new_target) end end it "should not replace the file if an exception is thrown in the block" do yielded = false threw = false begin subject.replace_file(target.path, 0600) do |fh| yielded = true fh.puts "different content written, then..." raise "...throw some random failure" end rescue Exception => e if e.to_s =~ /some random failure/ threw = true else raise end end yielded.should be_true threw.should be_true # ...and check the replacement was complete. File.read(target.path).should == "hello, world\n" end end end