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>
This commit is contained in:
parent
fc8663edc0
commit
39f9d3e4a6
4 changed files with 243 additions and 24 deletions
2
changelogs/fragments/gem-custom-home.yaml
Normal file
2
changelogs/fragments/gem-custom-home.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
new_features:
|
||||
- gem - add ability to specify a custom directory for installing gems (https://github.com/ansible/ansible/pull/38195)
|
|
@ -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']
|
||||
|
|
|
@ -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: verify module output properties
|
||||
- name: Ensure gem was installed
|
||||
assert:
|
||||
that:
|
||||
- "'name' in gem_result"
|
||||
- "'changed' in gem_result"
|
||||
- "'state' in gem_result"
|
||||
- install_gem_result is changed
|
||||
- current_gems.stdout is search('gist\s+\([0-9.]+\)')
|
||||
|
||||
- name: verify gist is installed
|
||||
shell: gem list | egrep '^gist '
|
||||
- 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: Ensure previous task failed
|
||||
assert:
|
||||
that:
|
||||
- install_gem_fail_result is failed
|
||||
- install_gem_fail_result.msg == 'install_dir requires user_install=false'
|
||||
|
||||
- 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
|
||||
|
|
121
test/units/modules/packaging/language/test_gem.py
Normal file
121
test/units/modules/packaging/language/test_gem.py
Normal file
|
@ -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'
|
Loading…
Reference in a new issue