summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAntoine Catton <devel@antoine.catton.fr>2018-05-21 15:55:43 +0200
committerSam Doran <sdoran@redhat.com>2018-05-21 09:55:43 -0400
commit39f9d3e4a683b1330f7d734802cf925952799dc3 (patch)
treebcf5d1c473e0e03cd63d6027135c0432612067d8
parentfc8663edc05e5fd15f9cd5dc98d1f71c9c338131 (diff)
downloadansible-39f9d3e4a683b1330f7d734802cf925952799dc3.tar.gz
Add the ability to specify an install_dir to the gem module (#38195)
* Add the ability to specify an install_dir to the gem module * Add GEM_HOME when installing a non-global gem * Add tests for custom gem path * Fix sanity tests * Add changelog entry * Rebase and add tests for incorrect options Co-authored by: Antoine Catton <devel@antoine.catton.fr>
-rw-r--r--changelogs/fragments/gem-custom-home.yaml2
-rw-r--r--lib/ansible/modules/packaging/language/gem.py29
-rw-r--r--test/integration/targets/gem/tasks/main.yml113
-rw-r--r--test/units/modules/packaging/language/test_gem.py121
4 files changed, 242 insertions, 23 deletions
diff --git a/changelogs/fragments/gem-custom-home.yaml b/changelogs/fragments/gem-custom-home.yaml
new file mode 100644
index 0000000000..8ecc4f7086
--- /dev/null
+++ b/changelogs/fragments/gem-custom-home.yaml
@@ -0,0 +1,2 @@
+new_features:
+ - gem - add ability to specify a custom directory for installing gems (https://github.com/ansible/ansible/pull/38195)
diff --git a/lib/ansible/modules/packaging/language/gem.py b/lib/ansible/modules/packaging/language/gem.py
index 57ffc239df..4ef651e0df 100644
--- a/lib/ansible/modules/packaging/language/gem.py
+++ b/lib/ansible/modules/packaging/language/gem.py
@@ -58,6 +58,13 @@ options:
- Override the path to the gem executable
required: false
version_added: "1.4"
+ install_dir:
+ description:
+ - Install the gems into a specific directory.
+ These gems will be independant from the global installed ones.
+ Specifying this requires user_install to be false.
+ required: false
+ version_added: "2.6"
env_shebang:
description:
- Rewrite the shebang line on installed scripts to use /usr/bin/env.
@@ -133,6 +140,12 @@ def get_rubygems_version(module):
return tuple(int(x) for x in match.groups())
+def get_rubygems_environ(module):
+ if module.params['install_dir']:
+ return {'GEM_HOME': module.params['install_dir']}
+ return None
+
+
def get_installed_versions(module, remote=False):
cmd = get_rubygems_path(module)
@@ -143,7 +156,9 @@ def get_installed_versions(module, remote=False):
cmd.extend(['--source', module.params['repository']])
cmd.append('-n')
cmd.append('^%s$' % module.params['name'])
- (rc, out, err) = module.run_command(cmd, check_rc=True)
+
+ environ = get_rubygems_environ(module)
+ (rc, out, err) = module.run_command(cmd, environ_update=environ, check_rc=True)
installed_versions = []
for line in out.splitlines():
match = re.match(r"\S+\s+\((.+)\)", line)
@@ -155,7 +170,6 @@ def get_installed_versions(module, remote=False):
def exists(module):
-
if module.params['state'] == 'latest':
remoteversions = get_installed_versions(module, remote=True)
if remoteversions:
@@ -175,14 +189,18 @@ def uninstall(module):
if module.check_mode:
return
cmd = get_rubygems_path(module)
+ environ = get_rubygems_environ(module)
cmd.append('uninstall')
+ if module.params['install_dir']:
+ cmd.extend(['--install-dir', module.params['install_dir']])
+
if module.params['version']:
cmd.extend(['--version', module.params['version']])
else:
cmd.append('--all')
cmd.append('--executable')
cmd.append(module.params['name'])
- module.run_command(cmd, check_rc=True)
+ module.run_command(cmd, environ_update=environ, check_rc=True)
def install(module):
@@ -211,6 +229,8 @@ def install(module):
cmd.append('--user-install')
else:
cmd.append('--no-user-install')
+ if module.params['install_dir']:
+ cmd.extend(['--install-dir', module.params['install_dir']])
if module.params['pre_release']:
cmd.append('--pre')
if not module.params['include_doc']:
@@ -238,6 +258,7 @@ def main():
repository=dict(required=False, aliases=['source'], type='str'),
state=dict(required=False, default='present', choices=['present', 'absent', 'latest'], type='str'),
user_install=dict(required=False, default=True, type='bool'),
+ install_dir=dict(required=False, type='path'),
pre_release=dict(required=False, default=False, type='bool'),
include_doc=dict(required=False, default=False, type='bool'),
env_shebang=dict(required=False, default=False, type='bool'),
@@ -252,6 +273,8 @@ def main():
module.fail_json(msg="Cannot specify version when state=latest")
if module.params['gem_source'] and module.params['state'] == 'latest':
module.fail_json(msg="Cannot maintain state=latest when installing from local source")
+ if module.params['user_install'] and module.params['install_dir']:
+ module.fail_json(msg="install_dir requires user_install=false")
if not module.params['gem_source']:
module.params['gem_source'] = module.params['name']
diff --git a/test/integration/targets/gem/tasks/main.yml b/test/integration/targets/gem/tasks/main.yml
index 77b93e663d..924daa8930 100644
--- a/test/integration/targets/gem/tasks/main.yml
+++ b/test/integration/targets/gem/tasks/main.yml
@@ -25,31 +25,104 @@
- 'default.yml'
paths: '../vars'
-- name: install dependencies for test
- package: name={{ package_item }} state=present
- with_items: "{{ test_packages }}"
- loop_control:
- loop_var: package_item
+- name: Install dependencies for test
+ package:
+ name: "{{ item }}"
+ state: present
+ loop: "{{ test_packages }}"
when: ansible_distribution != "MacOSX"
-- name: remove a gem
- gem: name=gist state=absent
+- name: Install a gem
+ gem:
+ name: gist
+ state: present
+ register: install_gem_result
-- name: verify gist is not installed
- shell: gem list | egrep '^gist '
- register: uninstall
- failed_when: "uninstall.rc != 1"
+- name: List gems
+ command: gem list
+ register: current_gems
-- name: install a gem
- gem: name=gist state=present
- register: gem_result
+- name: Ensure gem was installed
+ assert:
+ that:
+ - install_gem_result is changed
+ - current_gems.stdout is search('gist\s+\([0-9.]+\)')
+
+- name: Remove a gem
+ gem:
+ name: gist
+ state: absent
+ register: remove_gem_results
+
+- name: List gems
+ command: gem list
+ register: current_gems
+
+- name: Verify gem is not installed
+ assert:
+ that:
+ - remove_gem_results is changed
+ - current_gems.stdout is not search('gist\s+\([0-9.]+\)')
+
+
+# Check cutom gem directory
+- name: Install gem in a custom directory with incorrect options
+ gem:
+ name: gist
+ state: present
+ install_dir: "{{ output_dir }}/gems"
+ ignore_errors: yes
+ register: install_gem_fail_result
+
+- debug:
+ var: install_gem_fail_result
+ tags: debug
-- name: verify module output properties
+- name: Ensure previous task failed
assert:
that:
- - "'name' in gem_result"
- - "'changed' in gem_result"
- - "'state' in gem_result"
+ - install_gem_fail_result is failed
+ - install_gem_fail_result.msg == 'install_dir requires user_install=false'
-- name: verify gist is installed
- shell: gem list | egrep '^gist '
+- name: Install a gem in a custom directory
+ gem:
+ name: gist
+ state: present
+ user_install: no
+ install_dir: "{{ output_dir }}/gems"
+ register: install_gem_result
+
+- name: Find gems in custom directory
+ find:
+ paths: "{{ output_dir }}/gems/gems"
+ file_type: directory
+ contains: gist
+ register: gem_search
+
+- name: Ensure gem was installed in custom directory
+ assert:
+ that:
+ - install_gem_result is changed
+ - gem_search.files[0].path is search('gist-[0-9.]+')
+ ignore_errors: yes
+
+- name: Remove a gem in a custom directory
+ gem:
+ name: gist
+ state: absent
+ user_install: no
+ install_dir: "{{ output_dir }}/gems"
+ register: install_gem_result
+
+- name: Find gems in custom directory
+ find:
+ paths: "{{ output_dir }}/gems/gems"
+ file_type: directory
+ contains: gist
+ register: gem_search
+
+- name: Ensure gem was removed in custom directory
+ assert:
+ that:
+ - install_gem_result is changed
+ - gem_search.files | length == 0
diff --git a/test/units/modules/packaging/language/test_gem.py b/test/units/modules/packaging/language/test_gem.py
new file mode 100644
index 0000000000..4dae9ece50
--- /dev/null
+++ b/test/units/modules/packaging/language/test_gem.py
@@ -0,0 +1,121 @@
+# Copyright (c) 2018 Antoine Catton
+# MIT License (see licenses/MIT-license.txt or https://opensource.org/licenses/MIT)
+import copy
+import json
+
+import pytest
+
+from ansible.modules.packaging.language import gem
+from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
+
+
+def get_command(run_command):
+ """Generate the command line string from the patched run_command"""
+ args = run_command.call_args[0]
+ command = args[0]
+ return ' '.join(command)
+
+
+class TestGem(ModuleTestCase):
+ def setUp(self):
+ super(TestGem, self).setUp()
+ self.rubygems_path = ['/usr/bin/gem']
+ self.mocker.patch(
+ 'ansible.modules.packaging.language.gem.get_rubygems_path',
+ lambda module: copy.deepcopy(self.rubygems_path),
+ )
+
+ @pytest.fixture(autouse=True)
+ def _mocker(self, mocker):
+ self.mocker = mocker
+
+ def patch_installed_versions(self, versions):
+ """Mocks the versions of the installed package"""
+
+ target = 'ansible.modules.packaging.language.gem.get_installed_versions'
+
+ def new(module, remote=False):
+ return versions
+
+ return self.mocker.patch(target, new)
+
+ def patch_rubygems_version(self, version=None):
+ target = 'ansible.modules.packaging.language.gem.get_rubygems_version'
+
+ def new(module):
+ return version
+
+ return self.mocker.patch(target, new)
+
+ def patch_run_command(self):
+ target = 'ansible.module_utils.basic.AnsibleModule.run_command'
+ return self.mocker.patch(target)
+
+ def test_fails_when_user_install_and_install_dir_are_combined(self):
+ set_module_args({
+ 'name': 'dummy',
+ 'user_install': True,
+ 'install_dir': '/opt/dummy',
+ })
+
+ with pytest.raises(AnsibleFailJson) as exc:
+ gem.main()
+
+ result = exc.value.args[0]
+ assert result['failed']
+ assert result['msg'] == "install_dir requires user_install=false"
+
+ def test_passes_install_dir_to_gem(self):
+ # XXX: This test is extremely fragile, and makes assuptions about the module code, and how
+ # functions are run.
+ # If you start modifying the code of the module, you might need to modify what this
+ # test mocks. The only thing that matters is the assertion that this 'gem install' is
+ # invoked with '--install-dir'.
+
+ set_module_args({
+ 'name': 'dummy',
+ 'user_install': False,
+ 'install_dir': '/opt/dummy',
+ })
+
+ self.patch_rubygems_version()
+ self.patch_installed_versions([])
+ run_command = self.patch_run_command()
+
+ with pytest.raises(AnsibleExitJson) as exc:
+ gem.main()
+
+ result = exc.value.args[0]
+ assert result['changed']
+ assert run_command.called
+
+ assert '--install-dir /opt/dummy' in get_command(run_command)
+
+ def test_passes_install_dir_and_gem_home_when_uninstall_gem(self):
+ # XXX: This test is also extremely fragile because of mocking.
+ # If this breaks, the only that matters is to check whether '--install-dir' is
+ # in the run command, and that GEM_HOME is passed to the command.
+ set_module_args({
+ 'name': 'dummy',
+ 'user_install': False,
+ 'install_dir': '/opt/dummy',
+ 'state': 'absent',
+ })
+
+ self.patch_rubygems_version()
+ self.patch_installed_versions(['1.0.0'])
+
+ run_command = self.patch_run_command()
+
+ with pytest.raises(AnsibleExitJson) as exc:
+ gem.main()
+
+ result = exc.value.args[0]
+
+ assert result['changed']
+ assert run_command.called
+
+ assert '--install-dir /opt/dummy' in get_command(run_command)
+
+ update_environ = run_command.call_args[1].get('environ_update', {})
+ assert update_environ.get('GEM_HOME') == '/opt/dummy'