From 939e4b35551aa95270231aebc3ca26cb0d3b779e Mon Sep 17 00:00:00 2001 From: Rick Elrod Date: Mon, 13 Dec 2021 22:47:46 -0600 Subject: [PATCH] [configuration] delayed postprocs, extra volmounts Change: - Allow post-processors to be delayed until after the first (alphabetical) pass. In the delayed pass, delayed post-processors are again executed alphabetically. - In the interest of keeping configuration simple, we don't allow for multiple "levels" of delay. A post-processor is either run during the first pass, or delayed until after the first/normal pass. - This also adds an "extra volmounts" variable to the navigator post-processor class, which can be used by other post-processors to signify that they want a volume to be mounted. The volume-mount post processor is now delayed to accommodate. Test Plan: - Using this in lint/navigator work, no new tests yet because there is no other post processor currently which adds an extra volume mount. Signed-off-by: Rick Elrod --- .../configuration_subsystem/configurator.py | 10 +++ .../configuration_subsystem/definitions.py | 2 + .../navigator_configuration.py | 1 + .../navigator_post_processor.py | 69 ++++++++++++++++++- .../test_navigator_post_processor.py | 31 +++++++++ 5 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 tests/unit/configuration_subsystem/test_navigator_post_processor.py diff --git a/src/ansible_navigator/configuration_subsystem/configurator.py b/src/ansible_navigator/configuration_subsystem/configurator.py index 31c1db78c..054179b3a 100644 --- a/src/ansible_navigator/configuration_subsystem/configurator.py +++ b/src/ansible_navigator/configuration_subsystem/configurator.py @@ -234,7 +234,17 @@ def _apply_cli_params(self) -> None: self._messages.append(LogMessage(level=logging.INFO, message=message)) def _post_process(self) -> None: + delayed = [] + normal = [] + + # Separate normal and delayed entries so they can be processed in that order. for entry in self._config.entries: + if entry.delay_post_process: + delayed.append(entry) + else: + normal.append(entry) + + for entry in normal + delayed: if self._initial or entry.change_after_initial: processor = getattr(self._config.post_processor, entry.name, None) if callable(processor): diff --git a/src/ansible_navigator/configuration_subsystem/definitions.py b/src/ansible_navigator/configuration_subsystem/definitions.py index 9df9a5b18..6f1209d1a 100644 --- a/src/ansible_navigator/configuration_subsystem/definitions.py +++ b/src/ansible_navigator/configuration_subsystem/definitions.py @@ -60,6 +60,7 @@ class Entry(SimpleNamespace): apply_to_subsequent_cli: Should this be applied to future CLIs parsed choice: valid choices for this entry cli_parameters: argparse specific params + delay_post_process: Post process in normal (alphabetical) order or wait until after first pass? environment_variable_override: override the default environment variable name: the reference name for the entry settings_file_path_override: over the default settings file path @@ -76,6 +77,7 @@ class Entry(SimpleNamespace): change_after_initial: bool = True choices: List = [] cli_parameters: Union[None, CliParameters] = None + delay_post_process: bool = False environment_variable_override: Union[None, str] = None settings_file_path_override: Union[None, str] = None subcommands: Union[List[str], Constants] = Constants.ALL diff --git a/src/ansible_navigator/configuration_subsystem/navigator_configuration.py b/src/ansible_navigator/configuration_subsystem/navigator_configuration.py index 4c7d93fef..8b242cb11 100644 --- a/src/ansible_navigator/configuration_subsystem/navigator_configuration.py +++ b/src/ansible_navigator/configuration_subsystem/navigator_configuration.py @@ -291,6 +291,7 @@ class Internals(SimpleNamespace): Entry( name="execution_environment_volume_mounts", cli_parameters=CliParameters(action="append", nargs="+", short="--eev"), + delay_post_process=True, settings_file_path_override="execution-environment.volume-mounts", short_description=( "Specify volume to be bind mounted within an execution environment" diff --git a/src/ansible_navigator/configuration_subsystem/navigator_post_processor.py b/src/ansible_navigator/configuration_subsystem/navigator_post_processor.py index 77ce4a2ee..31a784c0b 100644 --- a/src/ansible_navigator/configuration_subsystem/navigator_post_processor.py +++ b/src/ansible_navigator/configuration_subsystem/navigator_post_processor.py @@ -6,6 +6,8 @@ import shlex import shutil +from dataclasses import dataclass, field +from enum import Enum from pathlib import Path from typing import List from typing import Tuple @@ -46,6 +48,51 @@ def wrapper(*args, **kwargs): return wrapper +class VolumeMountOption(Enum): + """Options that can be tagged on to the end of volume mounts. + + Usually these are used for things like selinux relabeling, but there are + some other valid options as well, which can and should be added here as + needed. See ``man podman-run`` and ``man docker-run`` for valid choices and + keep in mind that we support both runtimes. + """ + + # Relabel as private + Z = "Z" + + # Relabel as shared. + z = "z" # pylint: disable=invalid-name + + +@dataclass +class VolumeMount: + """Describes EE volume mounts.""" + + #: The name of the config option requiring this volume mount. + calling_option: str + + #: The source path of the volume mount. + src: str + + #: The destination path in the container for the volume mount. + dest: str + + #: Options for the bind mount. + options: List[VolumeMountOption] = field(default_factory=list) + + def exists(self) -> bool: + """Determine if the volume mount source exists.""" + return Path(self.src).exists() + + def to_string(self) -> str: + """Render the volume mount in a way that (docker|podman) understands.""" + out = f"{self.src}:{self.dest}" + if self.options: + joined_opts = ",".join(o.value for o in self.options) # pylint: disable=not-an-iterable + out += f":{joined_opts}" + return out + + PostProcessorReturn = Tuple[List[LogMessage], List[ExitMessage]] @@ -53,6 +100,12 @@ class NavigatorPostProcessor: # pylint:disable=too-many-public-methods """application post processor""" + def __init__(self): + # Volume mounts accumulated from post processing various config entries. + # These get processed towards the end, in the (delayed) + # execution_environment_volume_mounts() post-processor. + self.extra_volume_mounts: List[VolumeMount] = [] + @staticmethod def _true_or_false(entry: Entry, config: ApplicationConfiguration) -> PostProcessorReturn: # pylint: disable=unused-argument @@ -233,12 +286,12 @@ def execution_environment_image( entry.value.current = f"{entry.value.current}:latest" return messages, exit_messages - @staticmethod @_post_processor def execution_environment_volume_mounts( - entry: Entry, config: ApplicationConfiguration + self, entry: Entry, config: ApplicationConfiguration ) -> PostProcessorReturn: # pylint: disable=unused-argument + # pylint: disable=too-many-branches """Post process set_environment_variable""" messages: List[LogMessage] = [] exit_messages: List[ExitMessage] = [] @@ -305,6 +358,18 @@ def execution_environment_volume_mounts( return messages, exit_messages entry.value.current = parsed_volume_mounts + + if self.extra_volume_mounts and entry.value.current is C.NOT_SET: + entry.value.current = [] + + for mount in self.extra_volume_mounts: + if not mount.exists(): + exit_msg = ( + f"The volume mount source path '{mount.src}', needed by " + f"{mount.calling_option}, does not exist." + ) + exit_messages.append(ExitMessage(message=exit_msg)) + entry.value.current.append(mount.to_string()) return messages, exit_messages @staticmethod diff --git a/tests/unit/configuration_subsystem/test_navigator_post_processor.py b/tests/unit/configuration_subsystem/test_navigator_post_processor.py new file mode 100644 index 000000000..a7ee0f00f --- /dev/null +++ b/tests/unit/configuration_subsystem/test_navigator_post_processor.py @@ -0,0 +1,31 @@ +"""Tests for the navigator config post-processor.""" + +import pytest + +from ansible_navigator.configuration_subsystem.navigator_post_processor import ( + VolumeMount, + VolumeMountOption, +) + + +@pytest.mark.parametrize( + ("volmount", "expected"), + ( + (VolumeMount("test_option", "/foo", "/bar"), "/foo:/bar"), + (VolumeMount("test_option", "/foo", "/bar", [VolumeMountOption.z]), "/foo:/bar:z"), + ( + VolumeMount("test_option", "/foo", "/bar", [VolumeMountOption.z, VolumeMountOption.Z]), + "/foo:/bar:z,Z", + ), + (VolumeMount("test_option", "/foo", "/bar", []), "/foo:/bar"), + ), + ids=( + "normal mount", + "mount with relabel option", + "mount with a list of options", + "mount with empty list of options", + ), +) +def test_navigator_volume_mount_to_string(volmount, expected): + """Make sure volume mount ``to_string`` is sane.""" + assert volmount.to_string() == expected