diff --git a/docs/changelog-fragments.d/526.feature.md b/docs/changelog-fragments.d/526.feature.md new file mode 100644 index 000000000..f7b45685a --- /dev/null +++ b/docs/changelog-fragments.d/526.feature.md @@ -0,0 +1,9 @@ +Added the ability to run ad-hoc commands directly within an execution +environment using mode `stdout`. + +Here is an example invocation: +```bash +ansible-navigator exec --mode stdout +``` + +-- by {user}`cidrblock` diff --git a/src/ansible_navigator/actions/_actions.py b/src/ansible_navigator/actions/_actions.py index bf1ded6f8..26660b201 100644 --- a/src/ansible_navigator/actions/_actions.py +++ b/src/ansible_navigator/actions/_actions.py @@ -3,6 +3,7 @@ import functools import importlib +import logging import re from collections import namedtuple @@ -19,6 +20,8 @@ import importlib_resources as resources # type: ignore[import, no-redef] +logger = logging.getLogger(__name__) + # Basic structure for storing information about one action ActionT = namedtuple("ActionT", ("name", "cls", "kegex")) @@ -95,7 +98,12 @@ def run_interactive(package: str, action: str, *args: Any, **_kwargs: Any) -> An """Call the given action's run""" action_cls = get(package, action) app, interaction = args - return action_cls(app.args).run(app=app, interaction=interaction) + app_action = action_cls(app.args) + supports_interactive = hasattr(app_action, "run") + if not supports_interactive: + logger.error("Subcommand '%s' does not support mode interactive", action) + run_action = app_action.run if supports_interactive else app_action.no_interactive_mode + return run_action(app=app, interaction=interaction) def run_interactive_factory(package: str) -> Callable: diff --git a/src/ansible_navigator/actions/exec.py b/src/ansible_navigator/actions/exec.py new file mode 100644 index 000000000..9a628c38b --- /dev/null +++ b/src/ansible_navigator/actions/exec.py @@ -0,0 +1,116 @@ +"""Run the :exec subcommand.""" +import os +import shlex + +from typing import List +from typing import Optional +from typing import Tuple +from typing import Union + +from . import _actions as actions +from ..app import App +from ..configuration_subsystem import ApplicationConfiguration +from ..configuration_subsystem.definitions import Constants +from ..runner import Command + +GeneratedCommand = Tuple[str, Optional[List[str]]] + + +def _generate_command(exec_command: str, exec_shell: bool) -> GeneratedCommand: + """Generate the command and args. + + :param exec_command: The command to run + :param exec_shell: Should the command be wrapped in a shell + :returns: The command and any pass through arguments + """ + pass_through_args = None + if exec_shell and exec_command: + command = "/bin/bash" + pass_through_args = ["-c", exec_command] + else: + parts = shlex.split(exec_command) + command = parts[0] + if len(parts) > 1: + pass_through_args = parts[1:] + return (command, pass_through_args) + + +@actions.register +class Action(App): + """Run the :exec subcommand.""" + + # pylint: disable=too-few-public-methods + + KEGEX = "^e(?:xec)?$" + + def __init__(self, args: ApplicationConfiguration): + """Initialize the action. + + :param args: The current application configuration. + """ + super().__init__(args=args, logger_name=__name__, name="exec") + + def run_stdout(self) -> Union[None, int]: + """Run in mode stdout. + + :returns: The return code or None + """ + self._logger.debug("exec requested in stdout mode") + response = self._run_runner() + if response: + _, _, ret_code = response + return ret_code + return None + + def _run_runner(self) -> Optional[Tuple]: + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + """Spin up runner. + + :return: The stdout, stderr and return code from runner + """ + if isinstance(self._args.set_environment_variable, dict): + envvars_to_set = self._args.set_environment_variable.copy() + elif isinstance(self._args.set_environment_variable, Constants): + envvars_to_set = {} + else: + log_message = ( + "The setting 'set_environment_variable' was neither a dictionary" + " or Constants, please raise an issue. No environment variables will be set." + ) + self._logger.error( + "%s The current value was found to be '%s'", + log_message, + self._args.set_environment_variable, + ) + envvars_to_set = {} + + if self._args.display_color is False: + envvars_to_set["ANSIBLE_NOCOLOR"] = "1" + + kwargs = { + "container_engine": self._args.container_engine, + "host_cwd": os.getcwd(), + "execution_environment_image": self._args.execution_environment_image, + "execution_environment": self._args.execution_environment, + "navigator_mode": self._args.mode, + "pass_environment_variable": self._args.pass_environment_variable, + "set_environment_variable": envvars_to_set, + "timeout": self._args.ansible_runner_timeout, + } + + if isinstance(self._args.execution_environment_volume_mounts, list): + kwargs["container_volume_mounts"] = self._args.execution_environment_volume_mounts + + if isinstance(self._args.container_options, list): + kwargs["container_options"] = self._args.container_options + + command, pass_through_args = _generate_command( + exec_command=self._args.exec_command, + exec_shell=self._args.exec_shell, + ) + if isinstance(pass_through_args, list): + kwargs["cmdline"] = pass_through_args + + runner = Command(executable_cmd=command, **kwargs) + return runner.run() diff --git a/src/ansible_navigator/app.py b/src/ansible_navigator/app.py index d814e1c86..8c23a6d0d 100644 --- a/src/ansible_navigator/app.py +++ b/src/ansible_navigator/app.py @@ -20,6 +20,7 @@ from .ui_framework import Interaction from .ui_framework import ui +from .ui_framework import warning_notification from .utils import LogMessage from .utils import ExitMessage @@ -83,6 +84,19 @@ def app(self) -> AppPublic: ) raise AttributeError("app passed without args initialized") + def no_interactive_mode(self, interaction: Interaction, app: AppPublic) -> None: + # pylint: disable=unused-argument + """Show a warning notification that the user interactive mode is not supported.""" + warning = warning_notification( + messages=[ + f"The '{self._name}' subcommand is not available while using interactive mode.", + "[HINT] Start an additional instance of ansible-navigator" + + " in a new terminal with mode 'stdout'.", + f" e.g. 'ansible-navigator {self._name} --mode stdout", + ] + ) + interaction.ui.show(warning) + @staticmethod def _copy_args(args: ApplicationConfiguration) -> ApplicationConfiguration: """Deepcopy the args. diff --git a/src/ansible_navigator/configuration_subsystem/navigator_configuration.py b/src/ansible_navigator/configuration_subsystem/navigator_configuration.py index b22a36b42..4c7d93fef 100644 --- a/src/ansible_navigator/configuration_subsystem/navigator_configuration.py +++ b/src/ansible_navigator/configuration_subsystem/navigator_configuration.py @@ -116,6 +116,16 @@ class Internals(SimpleNamespace): " 'ansible-navigator doc --help-doc --mode stdout'" ), ), + SubCommand( + name="exec", + description="Run a command within an execution environment", + epilog=( + "Note: During development, it may become necessary to interact" + " directly with the execution environment to review and confirm" + " its build and behavior. All navigator settings will be applied" + " when starting the execution environment." + ), + ), SubCommand( name="images", description="Explore execution environment images", @@ -246,6 +256,23 @@ class Internals(SimpleNamespace): short_description="Specify if the editor is console based", value=EntryValue(default=True), ), + Entry( + name="exec_command", + cli_parameters=CliParameters(short="--excmd"), + settings_file_path_override="exec.command", + short_description="Specify the command to run within the execution environment", + subcommands=["exec"], + value=EntryValue(default="/bin/bash"), + ), + Entry( + name="exec_shell", + choices=[True, False], + cli_parameters=CliParameters(short="--exshell"), + settings_file_path_override="exec.shell", + short_description="Specify the exec command should be run in a shell.", + subcommands=["exec"], + value=EntryValue(default=True), + ), Entry( name="execution_environment", choices=[True, False], diff --git a/src/ansible_navigator/configuration_subsystem/navigator_post_processor.py b/src/ansible_navigator/configuration_subsystem/navigator_post_processor.py index 9e6110f16..77ce4a2ee 100644 --- a/src/ansible_navigator/configuration_subsystem/navigator_post_processor.py +++ b/src/ansible_navigator/configuration_subsystem/navigator_post_processor.py @@ -208,6 +208,16 @@ def execution_environment(self, entry, config) -> PostProcessorReturn: new_messages, new_exit_messages = check_for_ansible() messages.extend(new_messages) exit_messages.extend(new_exit_messages) + + if config.app == "exec": + exit_msg = "The 'exec' subcommand requires execution environment support." + exit_messages.append(ExitMessage(message=exit_msg)) + hint = ( + f"Try again with '{entry.cli_parameters.short} true'" + " to enable the use of an execution environment." + ) + exit_messages.append(ExitMessage(message=hint, prefix=ExitPrefix.HINT)) + return messages, exit_messages @staticmethod @@ -310,6 +320,17 @@ def container_options(entry: Entry, config: ApplicationConfiguration) -> PostPro entry.value.current = flatten_list(entry.value.current) return messages, exit_messages + @_post_processor + def exec_shell(self, entry: Entry, config: ApplicationConfiguration) -> PostProcessorReturn: + # pylint: disable=unused-argument + """Post process ``exec_shell``. + + :param entry: The current settings entry + :param config: The full application configuration + :return: An instance of the standard post process return object + """ + return self._true_or_false(entry, config) + @_post_processor def help_config(self, entry: Entry, config: ApplicationConfiguration) -> PostProcessorReturn: # pylint: disable=unused-argument diff --git a/tests/fixtures/integration/actions/exec/ansible-navigator.yaml b/tests/fixtures/integration/actions/exec/ansible-navigator.yaml new file mode 100644 index 000000000..62742db64 --- /dev/null +++ b/tests/fixtures/integration/actions/exec/ansible-navigator.yaml @@ -0,0 +1,4 @@ +ansible-navigator: + exec: + command: echo test_data_from_config + shell: False diff --git a/tests/fixtures/integration/actions/exec/test_stdout_config_cli.py/test/0.json b/tests/fixtures/integration/actions/exec/test_stdout_config_cli.py/test/0.json new file mode 100644 index 000000000..a29e70993 --- /dev/null +++ b/tests/fixtures/integration/actions/exec/test_stdout_config_cli.py/test/0.json @@ -0,0 +1,19 @@ +{ + "name": "test[exec echo with ee clear && ansible-navigator exec --excmd 'echo test_data_from_cli' --ee True --ll debug --mode stdout]", + "index": 0, + "comment": "exec echo with ee", + "additional_information": { + "look_fors": [ + "bash", + "test_data_from_cli" + ], + "look_nots": [ + "ERROR" + ], + "compared_fixture": false + }, + "output": [ + "test_data_from_cli", + "(venv) bash-5.1$" + ] +} diff --git a/tests/fixtures/integration/actions/exec/test_stdout_config_cli.py/test/1.json b/tests/fixtures/integration/actions/exec/test_stdout_config_cli.py/test/1.json new file mode 100644 index 000000000..1e3b11215 --- /dev/null +++ b/tests/fixtures/integration/actions/exec/test_stdout_config_cli.py/test/1.json @@ -0,0 +1,23 @@ +{ + "name": "test[exec echo without ee clear && ansible-navigator exec --excmd 'echo test_data_from_cli' --ee False --ll debug --mode stdout]", + "index": 1, + "comment": "exec echo without ee", + "additional_information": { + "look_fors": [ + "bash", + "test_data_from_cli", + "ERROR", + "requires execution environment support" + ], + "look_nots": [], + "compared_fixture": false + }, + "output": [ + "[ERROR]: Command provided: 'exec --excmd 'echo test_data_from_cli' --ee False --ll debug --mode stdout'", + "[ERROR]: The 'exec' subcommand requires execution environment support.", + " [HINT]: Try again with '--ee true' to enable the use of an execution environment.", + "[ERROR]: Configuration failed, using default log file location: /home/user/github/ansible-navigator/ansible-navigator.log. Log level set to debug", + " [HINT]: Review the hints and log file to see what went wrong: /home/user/github/ansible-navigator/ansible-navigator.log", + "(venv) bash-5.1$" + ] +} diff --git a/tests/fixtures/integration/actions/exec/test_stdout_config_cli.py/test/2.json b/tests/fixtures/integration/actions/exec/test_stdout_config_cli.py/test/2.json new file mode 100644 index 000000000..6d5b3ec8b --- /dev/null +++ b/tests/fixtures/integration/actions/exec/test_stdout_config_cli.py/test/2.json @@ -0,0 +1,16 @@ +{ + "name": "test[exec echo check path via shell clear && ansible-navigator exec --excmd 'echo $PATH' --ee True --ll debug --mode stdout]", + "index": 2, + "comment": "exec echo check path via shell", + "additional_information": { + "look_fors": [ + "/sbin" + ], + "look_nots": [], + "compared_fixture": false + }, + "output": [ + "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "(venv) bash-5.1$" + ] +} diff --git a/tests/fixtures/integration/actions/exec/test_stdout_config_cli.py/test/3.json b/tests/fixtures/integration/actions/exec/test_stdout_config_cli.py/test/3.json new file mode 100644 index 000000000..5e18ccbe4 --- /dev/null +++ b/tests/fixtures/integration/actions/exec/test_stdout_config_cli.py/test/3.json @@ -0,0 +1,16 @@ +{ + "name": "test[exec echo check no path via shell clear && ansible-navigator exec --excmd 'echo $PATH' --exshell false --ee True --ll debug --mode stdout]", + "index": 3, + "comment": "exec echo check no path via shell", + "additional_information": { + "look_fors": [ + "$PATH" + ], + "look_nots": [], + "compared_fixture": false + }, + "output": [ + "$PATH", + "(venv) bash-5.1$" + ] +} diff --git a/tests/fixtures/integration/actions/exec/test_stdout_config_file.py/test/0.json b/tests/fixtures/integration/actions/exec/test_stdout_config_file.py/test/0.json new file mode 100644 index 000000000..9aa22a49b --- /dev/null +++ b/tests/fixtures/integration/actions/exec/test_stdout_config_file.py/test/0.json @@ -0,0 +1,19 @@ +{ + "name": "test[exec echo with ee clear && ansible-navigator exec --ee True --ll debug --mode stdout]", + "index": 0, + "comment": "exec echo with ee", + "additional_information": { + "look_fors": [ + "bash", + "test_data_from_config" + ], + "look_nots": [ + "ERROR" + ], + "compared_fixture": false + }, + "output": [ + "test_data_from_config", + "(venv) bash-5.1$" + ] +} diff --git a/tests/fixtures/integration/actions/exec/test_stdout_config_file.py/test/1.json b/tests/fixtures/integration/actions/exec/test_stdout_config_file.py/test/1.json new file mode 100644 index 000000000..71a459175 --- /dev/null +++ b/tests/fixtures/integration/actions/exec/test_stdout_config_file.py/test/1.json @@ -0,0 +1,16 @@ +{ + "name": "test[exec echo check no path via shell clear && ansible-navigator exec --excmd '/usr/bin/echo $PATH' --ee True --ll debug --mode stdout]", + "index": 1, + "comment": "exec echo check no path via shell", + "additional_information": { + "look_fors": [], + "look_nots": [ + "/sbin" + ], + "compared_fixture": false + }, + "output": [ + "$PATH", + "(venv) bash-5.1$" + ] +} diff --git a/tests/fixtures/unit/configuration_subsystem/ansible-navigator.yml b/tests/fixtures/unit/configuration_subsystem/ansible-navigator.yml index 298a69f25..3b6ae3138 100644 --- a/tests/fixtures/unit/configuration_subsystem/ansible-navigator.yml +++ b/tests/fixtures/unit/configuration_subsystem/ansible-navigator.yml @@ -15,13 +15,16 @@ ansible-navigator: color: enable: False osc4: False - editor: - command: vim_from_setting - console: False documentation: plugin: name: shell type: become + editor: + command: vim_from_setting + console: False + exec: + shell: False + command: /bin/foo execution-environment: container-engine: podman enabled: False diff --git a/tests/integration/actions/exec/__init__.py b/tests/integration/actions/exec/__init__.py new file mode 100644 index 000000000..93c18b31b --- /dev/null +++ b/tests/integration/actions/exec/__init__.py @@ -0,0 +1 @@ +"""Integration tests for the ``exec`` subcommand.""" diff --git a/tests/integration/actions/exec/base.py b/tests/integration/actions/exec/base.py new file mode 100644 index 000000000..71fd0e315 --- /dev/null +++ b/tests/integration/actions/exec/base.py @@ -0,0 +1,110 @@ +"""The base class for exec interactive and stdout tests.""" + +import difflib +import json +import os + +from pathlib import Path +from typing import Generator +from typing import Union + +import pytest + +from ..._common import fixture_path_from_request +from ..._common import update_fixtures +from ..._tmux_session import TmuxSession +from ..._interactions import SearchFor +from ..._interactions import Step +from ....defaults import FIXTURES_DIR + +TEST_FIXTURE_DIR = Path(FIXTURES_DIR, "integration", "actions", "exec") +TEST_CONFIG_FILE = Path(TEST_FIXTURE_DIR, "ansible-navigator.yaml") + + +class BaseClass: + """The base class for interactive/stdout exec tests.""" + + update_fixtures = False + pane_height = 25 + pane_width = 300 + config_file: Union[Path, None] = None + + @pytest.fixture(scope="module", name="tmux_session") + def fixture_tmux_session( + self, + request: pytest.FixtureRequest, + ) -> Generator[TmuxSession, None, None]: + """Tmux fixture for this module. + + :param request: The request for this fixture + :yields: A tmux session + """ + tmux_params = { + "unique_test_id": request.node.nodeid, + "pane_height": self.pane_height, + "pane_width": self.pane_width, + } + if isinstance(self.config_file, Path): + assert self.config_file.exists() + tmux_params["config_path"] = self.config_file + + with TmuxSession(**tmux_params) as tmux_session: + yield tmux_session + + def test(self, request: pytest.FixtureRequest, tmux_session: TmuxSession, step: Step): + """Test interactive/stdout exec. + + :param request: The test request + :param tmux_session: The tmux session + :param step: A step within a series of tests + :raises ValueError: If test mode isn't set + """ + if step.search_within_response is not SearchFor.PROMPT: + raise ValueError("test mode not set") + + search_within_response = tmux_session.cli_prompt + + received_output = tmux_session.interaction( + value=step.user_input, + search_within_response=search_within_response, + ) + + fixtures_update_requested = ( + self.update_fixtures + or os.environ.get("ANSIBLE_NAVIGATOR_UPDATE_TEST_FIXTURES") == "true" + ) + if fixtures_update_requested: + update_fixtures( + request, + step.step_index, + received_output, + step.comment, + additional_information={ + "look_fors": step.look_fors, + "look_nots": step.look_nots, + "compared_fixture": not any((step.look_fors, step.look_nots)), + }, + ) + + page = " ".join(received_output) + + if step.look_fors: + assert all(look_for in page for look_for in step.look_fors) + + if step.look_nots: + assert not any(look_not in page for look_not in step.look_nots) + + if not any((step.look_fors, step.look_nots)): + dir_path, file_name = fixture_path_from_request(request, step.step_index) + with open(f"{dir_path}/{file_name}") as infile: + expected_output = json.load(infile)["output"] + + diff = "\n".join( + difflib.unified_diff( + expected_output, + received_output, + "expected", + "received", + ), + ) + assert expected_output == received_output, f"\n{diff}" diff --git a/tests/integration/actions/exec/test_stdout_config_cli.py b/tests/integration/actions/exec/test_stdout_config_cli.py new file mode 100644 index 000000000..dd61799bb --- /dev/null +++ b/tests/integration/actions/exec/test_stdout_config_cli.py @@ -0,0 +1,79 @@ +"""Tests for exec, mode stdout, parameters set using cli.""" + +import pytest + +from .base import BaseClass + +from ..._interactions import Command +from ..._interactions import SearchFor +from ..._interactions import Step +from ..._interactions import add_indicies + + +class StdoutCommand(Command): + """A stdout command.""" + + mode = "stdout" + subcommand = "exec" + preclear = True + + +class ShellCommand(Step): + """A shell command.""" + + search_within_response = SearchFor.PROMPT + + +stdout_tests = ( + ShellCommand( + comment="exec echo with ee", + user_input=StdoutCommand( + cmdline="--excmd 'echo test_data_from_cli'", + execution_environment=True, + ).join(), + look_fors=["bash", "test_data_from_cli"], + look_nots=["ERROR"], + ), + ShellCommand( + comment="exec echo without ee", + user_input=StdoutCommand( + cmdline="--excmd 'echo test_data_from_cli'", + execution_environment=False, + ).join(), + look_fors=["bash", "test_data_from_cli", "ERROR", "requires execution environment support"], + ), + ShellCommand( + comment="exec echo check path via shell", + user_input=StdoutCommand( + cmdline="--excmd 'echo $PATH'", + execution_environment=True, + ).join(), + look_fors=["/sbin"], + ), + ShellCommand( + comment="exec echo check no path via shell", + user_input=StdoutCommand( + cmdline="--excmd 'echo $PATH' --exshell false", + execution_environment=True, + ).join(), + look_fors=["$PATH"], + ), +) + +steps = add_indicies(stdout_tests) + + +def step_id(test_value: ShellCommand) -> str: + """Return the test id from the test step object. + + :param test_value: The data from the test iteration + :returns: An id for the test + """ + return f"{test_value.comment} {test_value.user_input}" + + +@pytest.mark.parametrize("step", steps, ids=step_id) +class Test(BaseClass): + """Run the tests for exec, mode stdout, parameters set using cli.""" + + update_fixtures = False diff --git a/tests/integration/actions/exec/test_stdout_config_file.py b/tests/integration/actions/exec/test_stdout_config_file.py new file mode 100644 index 000000000..a0b95952e --- /dev/null +++ b/tests/integration/actions/exec/test_stdout_config_file.py @@ -0,0 +1,65 @@ +"""Tests for exec, mode stdout, parameters set using config file.""" + +import pytest + + +from .base import BaseClass +from .base import TEST_CONFIG_FILE + +from ..._interactions import Command +from ..._interactions import SearchFor +from ..._interactions import Step +from ..._interactions import add_indicies + + +class StdoutCommand(Command): + """A stdout command.""" + + mode = "stdout" + subcommand = "exec" + preclear = True + + +class ShellCommand(Step): + """A shell command.""" + + search_within_response = SearchFor.PROMPT + + +stdout_tests = ( + ShellCommand( + comment="exec echo with ee", + user_input=StdoutCommand( + execution_environment=True, + ).join(), + look_fors=["bash", "test_data_from_config"], + look_nots=["ERROR"], + ), + ShellCommand( + comment="exec echo check no path via shell", + user_input=StdoutCommand( + cmdline="--excmd '/usr/bin/echo $PATH'", + execution_environment=True, + ).join(), + look_nots=["/sbin"], + ), +) + +steps = add_indicies(stdout_tests) + + +def step_id(test_value: ShellCommand) -> str: + """Return the test id from the test step object. + + :param test_value: The data from the test iteration + :returns: An id for the test + """ + return f"{test_value.comment} {test_value.user_input}" + + +@pytest.mark.parametrize("step", steps, ids=step_id) +class Test(BaseClass): + """Run the tests for exec, mode stdout, parameters set using config file.""" + + config_file = TEST_CONFIG_FILE + update_fixtures = False diff --git a/tests/unit/actions/test_exec.py b/tests/unit/actions/test_exec.py new file mode 100644 index 000000000..c649d70b3 --- /dev/null +++ b/tests/unit/actions/test_exec.py @@ -0,0 +1,87 @@ +"""Some simple tests for exec command and param generation.""" + +from typing import List +from typing import NamedTuple + +import pytest + +from ansible_navigator.actions.exec import _generate_command + + +class CommandTestData(NamedTuple): + """The artifact files test data object.""" + + name: str + command: str + shell: bool + result_command: str + result_params: List + + +def id_from_data(test_value): + """Return the name from the test data object. + + :param test_value: The value from which the test id will be extracted + :returns: The test id + """ + return f" {test_value.name} " + + +command_test_data = [ + CommandTestData( + name="With shell simple", + command="echo foo", + shell=True, + result_command="/bin/bash", + result_params=["-c", "echo foo"], + ), + CommandTestData( + name="Without shell simple", + command="echo foo", + shell=False, + result_command="echo", + result_params=["foo"], + ), + CommandTestData( + name="With shell complex", + command="ansible-vault encrypt_string --vault-password-file" + + " a_password_file 'foobar' --name 'the_secret'", + shell=True, + result_command="/bin/bash", + result_params=[ + "-c", + "ansible-vault encrypt_string --vault-password-file" + + " a_password_file 'foobar' --name 'the_secret'", + ], + ), + CommandTestData( + name="Without shell complex", + command="ansible-vault encrypt_string --vault-password-file" + + " a_password_file 'foobar' --name 'the secret'", + shell=False, + result_command="ansible-vault", + result_params=[ + "encrypt_string", + "--vault-password-file", + "a_password_file", + "foobar", + "--name", + "the secret", + ], + ), +] + + +@pytest.mark.parametrize("cmd_test_data", command_test_data, ids=id_from_data) +def test_artifact_path(cmd_test_data: CommandTestData): + """Test the generation of the command and params. + + :param cmd_test_data: The test data + """ + command, additional_params = _generate_command( + exec_command=cmd_test_data.command, + exec_shell=cmd_test_data.shell, + ) + comment = command_test_data, command, additional_params + assert command == cmd_test_data.result_command, comment + assert additional_params == cmd_test_data.result_params, comment diff --git a/tests/unit/configuration_subsystem/data.py b/tests/unit/configuration_subsystem/data.py index 095757f27..519fbba48 100644 --- a/tests/unit/configuration_subsystem/data.py +++ b/tests/unit/configuration_subsystem/data.py @@ -219,6 +219,8 @@ def cli_data(): ("display_color", "yellow is the color of a banana", False), ("editor_command", "nano_envvar", "nano_envvar"), ("editor_console", "false", False), + ("exec_command", "/bin/foo", "/bin/foo"), + ("exec_shell", "false", False), ("execution_environment", "false", False), ("execution_environment_image", "test_image:latest", "test_image:latest"), ("execution_environment_volume_mounts", "/test1:/test1:Z", ["/test1:/test1:Z"]), diff --git a/tests/unit/configuration_subsystem/test_precedence.py b/tests/unit/configuration_subsystem/test_precedence.py index 8da14a45b..08a893cf4 100644 --- a/tests/unit/configuration_subsystem/test_precedence.py +++ b/tests/unit/configuration_subsystem/test_precedence.py @@ -10,6 +10,7 @@ """ import os +import shlex from unittest import mock from unittest.mock import patch @@ -45,10 +46,11 @@ def test_all_entries_reflect_cli_given_envvars( ): """Ensure all entries are set by the CLI, even with environment variables set.""" if base is None: - params = cli_entry.split() + params = shlex.split(cli_entry) expected = dict(expected) else: - params = cli_entry.split() + " ".join(base.splitlines()).split() + cli_entry_split = shlex.split(cli_entry) + params = cli_entry_split + " ".join(base.splitlines()).split() expected = {**dict(expected), **dict(BASE_EXPECTED)} envvars = {} @@ -82,10 +84,11 @@ def test_all_entries_reflect_cli_given_settings( either DEFAULT_CFG or USER_CFG """ if base is None: - params = cli_entry.split() + params = shlex.split(cli_entry) expected = dict(expected) else: - params = cli_entry.split() + " ".join(base.splitlines()).split() + cli_entry_split = shlex.split(cli_entry) + params = cli_entry_split + " ".join(base.splitlines()).split() expected = {**dict(expected), **dict(BASE_EXPECTED)} response = generate_config(params=params, setting_file_name=settings) @@ -118,10 +121,10 @@ def test_all_entries_reflect_cli_given_settings_and_envars( even though an empty or full settings file was provided """ if base is None: - params = cli_entry.split() + params = shlex.split(cli_entry) expected = dict(expected) else: - params = cli_entry.split() + " ".join(base.splitlines()).split() + params = shlex.split(cli_entry) + " ".join(base.splitlines()).split() expected = {**dict(expected), **dict(BASE_EXPECTED)} envvars = {}