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>pull/4420/head
parent
fc8663edc0
commit
39f9d3e4a6
|
@ -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
|
- Override the path to the gem executable
|
||||||
required: false
|
required: false
|
||||||
version_added: "1.4"
|
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:
|
env_shebang:
|
||||||
description:
|
description:
|
||||||
- Rewrite the shebang line on installed scripts to use /usr/bin/env.
|
- 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())
|
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):
|
def get_installed_versions(module, remote=False):
|
||||||
|
|
||||||
cmd = get_rubygems_path(module)
|
cmd = get_rubygems_path(module)
|
||||||
|
@ -143,7 +156,9 @@ def get_installed_versions(module, remote=False):
|
||||||
cmd.extend(['--source', module.params['repository']])
|
cmd.extend(['--source', module.params['repository']])
|
||||||
cmd.append('-n')
|
cmd.append('-n')
|
||||||
cmd.append('^%s$' % module.params['name'])
|
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 = []
|
installed_versions = []
|
||||||
for line in out.splitlines():
|
for line in out.splitlines():
|
||||||
match = re.match(r"\S+\s+\((.+)\)", line)
|
match = re.match(r"\S+\s+\((.+)\)", line)
|
||||||
|
@ -155,7 +170,6 @@ def get_installed_versions(module, remote=False):
|
||||||
|
|
||||||
|
|
||||||
def exists(module):
|
def exists(module):
|
||||||
|
|
||||||
if module.params['state'] == 'latest':
|
if module.params['state'] == 'latest':
|
||||||
remoteversions = get_installed_versions(module, remote=True)
|
remoteversions = get_installed_versions(module, remote=True)
|
||||||
if remoteversions:
|
if remoteversions:
|
||||||
|
@ -175,14 +189,18 @@ def uninstall(module):
|
||||||
if module.check_mode:
|
if module.check_mode:
|
||||||
return
|
return
|
||||||
cmd = get_rubygems_path(module)
|
cmd = get_rubygems_path(module)
|
||||||
|
environ = get_rubygems_environ(module)
|
||||||
cmd.append('uninstall')
|
cmd.append('uninstall')
|
||||||
|
if module.params['install_dir']:
|
||||||
|
cmd.extend(['--install-dir', module.params['install_dir']])
|
||||||
|
|
||||||
if module.params['version']:
|
if module.params['version']:
|
||||||
cmd.extend(['--version', module.params['version']])
|
cmd.extend(['--version', module.params['version']])
|
||||||
else:
|
else:
|
||||||
cmd.append('--all')
|
cmd.append('--all')
|
||||||
cmd.append('--executable')
|
cmd.append('--executable')
|
||||||
cmd.append(module.params['name'])
|
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):
|
def install(module):
|
||||||
|
@ -211,6 +229,8 @@ def install(module):
|
||||||
cmd.append('--user-install')
|
cmd.append('--user-install')
|
||||||
else:
|
else:
|
||||||
cmd.append('--no-user-install')
|
cmd.append('--no-user-install')
|
||||||
|
if module.params['install_dir']:
|
||||||
|
cmd.extend(['--install-dir', module.params['install_dir']])
|
||||||
if module.params['pre_release']:
|
if module.params['pre_release']:
|
||||||
cmd.append('--pre')
|
cmd.append('--pre')
|
||||||
if not module.params['include_doc']:
|
if not module.params['include_doc']:
|
||||||
|
@ -238,6 +258,7 @@ def main():
|
||||||
repository=dict(required=False, aliases=['source'], type='str'),
|
repository=dict(required=False, aliases=['source'], type='str'),
|
||||||
state=dict(required=False, default='present', choices=['present', 'absent', 'latest'], type='str'),
|
state=dict(required=False, default='present', choices=['present', 'absent', 'latest'], type='str'),
|
||||||
user_install=dict(required=False, default=True, type='bool'),
|
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'),
|
pre_release=dict(required=False, default=False, type='bool'),
|
||||||
include_doc=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'),
|
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")
|
module.fail_json(msg="Cannot specify version when state=latest")
|
||||||
if module.params['gem_source'] and module.params['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")
|
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']:
|
if not module.params['gem_source']:
|
||||||
module.params['gem_source'] = module.params['name']
|
module.params['gem_source'] = module.params['name']
|
||||||
|
|
|
@ -25,31 +25,104 @@
|
||||||
- 'default.yml'
|
- 'default.yml'
|
||||||
paths: '../vars'
|
paths: '../vars'
|
||||||
|
|
||||||
- name: install dependencies for test
|
- name: Install dependencies for test
|
||||||
package: name={{ package_item }} state=present
|
package:
|
||||||
with_items: "{{ test_packages }}"
|
name: "{{ item }}"
|
||||||
loop_control:
|
state: present
|
||||||
loop_var: package_item
|
loop: "{{ test_packages }}"
|
||||||
when: ansible_distribution != "MacOSX"
|
when: ansible_distribution != "MacOSX"
|
||||||
|
|
||||||
- name: remove a gem
|
- name: Install a gem
|
||||||
gem: name=gist state=absent
|
gem:
|
||||||
|
name: gist
|
||||||
|
state: present
|
||||||
|
register: install_gem_result
|
||||||
|
|
||||||
- name: verify gist is not installed
|
- name: List gems
|
||||||
shell: gem list | egrep '^gist '
|
command: gem list
|
||||||
register: uninstall
|
register: current_gems
|
||||||
failed_when: "uninstall.rc != 1"
|
|
||||||
|
|
||||||
- name: install a gem
|
- name: Ensure gem was installed
|
||||||
gem: name=gist state=present
|
|
||||||
register: gem_result
|
|
||||||
|
|
||||||
- name: verify module output properties
|
|
||||||
assert:
|
assert:
|
||||||
that:
|
that:
|
||||||
- "'name' in gem_result"
|
- install_gem_result is changed
|
||||||
- "'changed' in gem_result"
|
- current_gems.stdout is search('gist\s+\([0-9.]+\)')
|
||||||
- "'state' in gem_result"
|
|
||||||
|
|
||||||
- name: verify gist is installed
|
- name: Remove a gem
|
||||||
shell: gem list | egrep '^gist '
|
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
|
||||||
|
|
|
@ -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 New Issue