Skip to content

Commit

Permalink
Add support for global molecule configuration
Browse files Browse the repository at this point in the history
This patch adds the support for molecule base configuration. It will
allow us to use a global molecule configuration file[1] or to pass one
or multiple base configuration file(s) according to ansible-molecule
through the tox.ini file and the `molecule_config_files` option.

By default, if not passing base configuration file(s) in the tox.ini
file but if there is a global configuration file[1], this will be the
reference to determine the driver name if the scenarios configuration
files didn't override the default one coming from the global one[1].

If there is [1] at the project level and passing base configuration
file(s) in the tox.ini file, those latter will take precedence over the
global one[1].

Note that tox-ansible will now raise a RuntimeError if it finds out
multiple driver name declaration when using multiple molecule base
configuration.

[1] - <PROJECT_ABSPATH>/.config/molecule/config.yml

Fixes ansible#88

Signed-off-by: Gael Chamoulaud (Strider) <gchamoul@redhat.com>
  • Loading branch information
strider authored and greg-hellings committed Sep 3, 2021
1 parent b35810b commit db24dc9
Show file tree
Hide file tree
Showing 14 changed files with 195 additions and 5 deletions.
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,26 @@ spaces and other special characters to be used without needing shell-style escap
```ini
[ansible]
molecule_opts =
-c
{toxinidir}/tests/molecule.yml
--debug
```

If you use a global molecule configuration file at the project level
(`<project_name>/.config/molecule/config.yml`), it will be detected
automatically and will be the reference in order to determine the default driver
name used for your molecule scenarios.

If you want pass one or multiple base configuration file(s) to
"[molecule](https://github.com/ansible-community/molecule)", add the option
"molecule\_config\_files" to the Ansible section and list them as follows.
```ini
[ansible]
molecule_opts =
--debug
molecule_config_files =
{toxinidir}/tests/molecule_one.yml
{toxinidir}/tests/molecule_two.yml
```

Sometimes there are paths you will want to ignore running tests in. Particularly if you
install other roles or collections underneath of your source tree. You can ignore these paths
with the following tox.ini bit:
Expand Down
43 changes: 42 additions & 1 deletion src/tox_ansible/ansible/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from ..tox_molecule_case import ToxMoleculeCase
from .scenario import Scenario

LOCAL_CONFIG_FILE = ".config/molecule/config.yml"


class Ansible(object):
"""A generalized class that handles interactions between the plugin and
Expand All @@ -35,6 +37,24 @@ def __init__(self, base="", options=None):
self.options = options
self.tox = Tox()

def molecule_config_files(self):
"""Determine if there is a global molecule configuration file at the
project level. If there are molecule base configuration file(s) passed
as an option in the tox.ini file, those will take precedence over the
global configuration file at the project level.
:return: A list of absolute path of molecule base configuration file.
None, otherwise."""
global_molecule_config = path.join(self.directory, LOCAL_CONFIG_FILE)

if self.options.molecule_config_files:
return self.options.molecule_config_files

if path.isfile(global_molecule_config):
return [global_molecule_config]

return None

@property
def is_ansible(self):
"""Determine if the specified directory is an Ansible structure or not
Expand All @@ -44,6 +64,22 @@ def is_ansible(self):
path.join(self.directory, "galaxy.yml")
)

@property
def molecule_config(self):
"""Reads all the molecule base configuration files present and adds them
in the self.molecule_config field.
:return: A list of one or multiple dictionaries including the content of
the molecule base configuration file(s)
"""
configs = []
config_files_list = self.molecule_config_files()
if config_files_list:
for config_file in config_files_list:
configs.append(load_yaml(config_file))

return configs

@property
def scenarios(self):
"""Recursively searches the potential Ansible directory and looks for any
Expand Down Expand Up @@ -74,7 +110,12 @@ def scenarios(self):
if branch in self.options.ignore_paths:
ignored = True
if not ignored:
self._scenarios.append(Scenario(path.relpath(base_dir, self.directory)))
self._scenarios.append(
Scenario(
path.relpath(base_dir, self.directory),
self.molecule_config,
)
)
return self._scenarios

@property
Expand Down
29 changes: 27 additions & 2 deletions src/tox_ansible/ansible/scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
class Scenario(object):
"""Knows about scenarios."""

def __init__(self, directory):
def __init__(self, directory, global_config=None):
self.directory = directory
self.scenario_file = path.join(self.directory, "molecule.yml")
self.global_config = global_config

def __str__(self):
return "{}".format(self.name)
Expand All @@ -32,10 +33,34 @@ def name(self):
@property
def driver(self):
"""Reads the driver for this scenario, if one is defined.
If there is a driver found in the scenario configuration and if there
is a global configuration, the driver coming from the scenario
will be returned. Otherwise, the global driver.
:return: Driver name defined in molecule.yml or None"""
:return: Driver name defined in molecule.yml or None
:raise: A RuntimeError if the driver name is present in multiple
molecule base configuration files given as options in the
tox.ini file. Or if no driver configuration has been found.
"""
if self.config and "driver" in self.config and "name" in self.config["driver"]:
return self.config["driver"]["name"]

if self.global_config:
drivers_found_number = len(
[i for i, d in enumerate(self.global_config) if "driver" in d]
)
if drivers_found_number == 0:
raise RuntimeError("No driver configuration found.")

if drivers_found_number == 1:
return self.global_config[-1].get("driver")["name"]

if drivers_found_number > 1:
raise RuntimeError(
"Driver configuration is present in multiple "
"molecule base configuration files."
)

return None

@property
Expand Down
4 changes: 4 additions & 0 deletions src/tox_ansible/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
INI_IGNORE_PATHS = "ignore_path"
INI_ANSIBLE_LINT_CONFIG = "ansible_lint_config"
INI_YAMLLINT_CONFIG = "yamllint_config"
INI_MOLECULE_CONFIG_FILES = "molecule_config_files"
INI_SCENARIO_FORMAT = "scenario_format"
INI_SCENARIO_FORMAT_DEFAULT = "$path-$parent-$name"

Expand All @@ -36,6 +37,9 @@ def __init__(self, tox):
self.matrix = Matrix()
self.ansible_lint = self.reader.getstring(INI_ANSIBLE_LINT_CONFIG)
self.yamllint = self.reader.getstring(INI_YAMLLINT_CONFIG)
self.molecule_config_files = self.reader.getlist(
INI_MOLECULE_CONFIG_FILES, sep="\n"
)
self.scenario_format = self.reader.getstring(
INI_SCENARIO_FORMAT, INI_SCENARIO_FORMAT_DEFAULT
)
Expand Down
5 changes: 5 additions & 0 deletions src/tox_ansible/tox_molecule_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ def get_commands(self, options):
user does not configure them explicitly"""
molecule = ["molecule"]
molecule.extend(options.get_global_opts())

if options.molecule_config_files:
for config_file in options.molecule_config_files:
molecule.extend(["-c", config_file])

molecule.extend(["test", "-s", self.scenario.name])
tox = Tox()
molecule.extend(tox.posargs)
Expand Down
3 changes: 3 additions & 0 deletions tests/fixtures/collection/molecule.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
driver:
name: podman
6 changes: 6 additions & 0 deletions tests/fixtures/collection/tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
skipdist = true
envlist = lint_all

[ansible]
molecule_opts =
--debug
molecule_config_files =
{toxinidir}/molecule.yml

[testenv]
usedevelop = false
skip_install = true
3 changes: 3 additions & 0 deletions tests/fixtures/expand_collection/molecule_one.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
driver:
name: openstack
3 changes: 3 additions & 0 deletions tests/fixtures/expand_collection/molecule_two.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
provisioner:
name: ansible
3 changes: 3 additions & 0 deletions tests/fixtures/expand_collection/tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ ansible = 2.{8,9}
python = 2.7,3.8
molecule_opts =
--debug
molecule_config_files =
{toxinidir}/molecule_one.yml
{toxinidir}/molecule_two.yml
ignore_path =
ignored
ansible_lint_config = some/path.config
Expand Down
3 changes: 3 additions & 0 deletions tests/fixtures/not_collection/.config/molecule/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
driver:
name: podman
32 changes: 32 additions & 0 deletions tests/test_ansible.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def test_with_scenarios(mocker, folder, expected):
ansible = Ansible(base=folder)
ansible.options = mocker.Mock()
ansible.options.ignore_paths = []
ansible.options.molecule_config_files = []
# ansible._scenarios = scenarios # pylint: disable=protected-access
assert ansible.directory == os.path.realpath(folder)
assert ansible.is_ansible == expected
Expand All @@ -34,4 +35,35 @@ def test_scenarios_correct(mocker):
ansible = Ansible("tests/fixtures/collection")
ansible.options = mocker.Mock()
ansible.options.ignore_paths = []
ansible.options.molecule_config_files = []
assert len(ansible.scenarios) == 6


def test_scenarios_with_global_molecule_config(mocker):
ansible = Ansible("tests/fixtures/not_collection")
global_config = [os.path.join(ansible.directory, ".config/molecule/config.yml")]
ansible.options = mocker.Mock()
ansible.options.ignore_paths = []
ansible.options.molecule_config_files = []
assert ansible.molecule_config_files() == global_config


def test_scenarios_with_base_molecule_config(mocker):
ansible = Ansible("tests/fixtures/collection")
base_configs = [os.path.join(ansible.directory, "molecule.yml")]
ansible.options = mocker.Mock()
ansible.options.ignore_paths = []
ansible.options.molecule_config_files = base_configs
assert ansible.molecule_config_files() == base_configs


def test_scenarios_with_multiple_base_molecule_config(mocker):
ansible = Ansible("tests/fixtures/expand_collection")
base_configs = [
os.path.join(ansible.directory, "molecule_one.yml"),
os.path.join(ansible.directory, "molecule_two.yml"),
]
ansible.options = mocker.Mock()
ansible.options.ignore_paths = []
ansible.options.molecule_config_files = base_configs
assert ansible.molecule_config_files() == base_configs
16 changes: 16 additions & 0 deletions tests/test_scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,19 @@ def test_no_driver(no_driver):
s = Scenario(no_driver)
assert s.name == "no_driver"
assert s.driver is None


def test_driver_with_global_config(surprise):
global_config = [{"driver": {"name": "podman"}}]
s = Scenario(surprise, global_config)
assert s.name == "surprise"
assert str(s) == "surprise"
assert s.driver == "surprise"
assert s.requirements is None


def test_no_driver_with_global_config(no_driver):
global_config = [{"driver": {"name": "podman"}}]
s = Scenario(no_driver, global_config)
assert s.name == "no_driver"
assert s.driver == "podman"
31 changes: 31 additions & 0 deletions tests/test_tox_molecule_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,50 @@ def test_case_is_simple(config, opts, scenario, mocker):
Tox, "posargs", new_callable=mocker.PropertyMock, return_value=[]
)
t = ToxMoleculeCase(scenario)
opts.molecule_config_files = []
assert t.get_name() == "my_test"
assert t.get_working_dir() == ""
cmds = [["molecule", "test", "-s", scenario.name]]
assert t.get_commands(opts) == cmds
assert t.get_basepython() is None


def test_case_is_simple_with_config_files(config, opts, scenario, mocker):
base_configs = [
"/home/jdoe/my_ansible_collections/tests/molecule_one.yml",
"/home/jdoe/my_ansible_collections/tests/molecule_one.yml",
]
mocker.patch.object(Options, "get_global_opts", return_value=[])
mocker.patch.object(
Tox, "posargs", new_callable=mocker.PropertyMock, return_value=[]
)
t = ToxMoleculeCase(scenario)
opts.molecule_config_files = base_configs
assert t.get_name() == "my_test"
assert t.get_working_dir() == ""
cmds = [
[
"molecule",
"-c",
base_configs[0],
"-c",
base_configs[-1],
"test",
"-s",
scenario.name,
]
]
assert t.get_commands(opts) == cmds
assert t.get_basepython() is None


def test_case_has_global_opts(mocker, scenario, opts, config):
mocker.patch.object(Options, "get_global_opts", return_value=["-c", "derp"])
mocker.patch.object(
Tox, "posargs", new_callable=mocker.PropertyMock, return_value=[]
)
t = ToxMoleculeCase(scenario)
opts.molecule_config_files = []
cmds = [["molecule", "-c", "derp", "test", "-s", scenario.name]]
assert t.get_commands(opts) == cmds

Expand Down

0 comments on commit db24dc9

Please sign in to comment.