From 1121854eceabae58353a52ce48711e77be8544a0 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 15 Oct 2024 15:07:50 -0400 Subject: [PATCH] Add functionality to retrieve streamable arguments --- src/ansible_runner/config/_base.py | 69 ++++++++++++++++------------- src/ansible_runner/config/runner.py | 68 +++++++++++++++++----------- test/unit/config/test_runner.py | 22 +++++++++ 3 files changed, 102 insertions(+), 57 deletions(-) diff --git a/src/ansible_runner/config/_base.py b/src/ansible_runner/config/_base.py index c39a61ed..c6e6a819 100644 --- a/src/ansible_runner/config/_base.py +++ b/src/ansible_runner/config/_base.py @@ -62,6 +62,11 @@ class BaseExecutionMode(Enum): GENERIC_COMMANDS = 2 +# Metadata string values +class MetaValues(Enum): + STREAMABLE = 'streamable' + + @dataclass class BaseConfig: """The base configuration object. @@ -77,38 +82,38 @@ class BaseConfig: # No other config objects make use of positional parameters, so this should be fine. # # Example use case: RunnerConfig("/tmp/demo", playbook="main.yml", ...) - private_data_dir: str | None = field(metadata={}, default=None) - - artifact_dir: str | None = field(metadata={}, default=None) - check_job_event_data: bool = field(metadata={}, default=False) - container_auth_data: dict[str, str] | None = field(metadata={}, default=None) - container_image: str = field(metadata={}, default="") - container_options: list[str] | None = field(metadata={}, default=None) - container_volume_mounts: list[str] | None = field(metadata={}, default=None) - container_workdir: str | None = field(metadata={}, default=None) - envvars: dict[str, Any] | None = field(metadata={}, default=None) - fact_cache: str | None = field(metadata={}, default=None) - fact_cache_type: str = field(metadata={}, default='jsonfile') - host_cwd: str | None = field(metadata={}, default=None) - ident: str | None = field(metadata={}, default=None) - json_mode: bool = field(metadata={}, default=False) - keepalive_seconds: int | None = field(metadata={}, default=None) - passwords: dict[str, str] | None = field(metadata={}, default=None) - process_isolation: bool = field(metadata={}, default=False) - process_isolation_executable: str = field(metadata={}, default=defaults.default_process_isolation_executable) - project_dir: str | None = field(metadata={}, default=None) - quiet: bool = field(metadata={}, default=False) - rotate_artifacts: int = field(metadata={}, default=0) - settings: dict | None = field(metadata={}, default=None) - ssh_key: str | None = field(metadata={}, default=None) - suppress_env_files: bool = field(metadata={}, default=False) - timeout: int | None = field(metadata={}, default=None) - - event_handler: Callable[[dict], None] | None = None - status_handler: Callable[[dict, BaseConfig], bool] | None = None - artifacts_handler: Callable[[str], None] | None = None - cancel_callback: Callable[[], bool] | None = None - finished_callback: Callable[[BaseConfig], None] | None = None + private_data_dir: str | None = field(metadata={MetaValues.STREAMABLE: False}, default=None) + + artifact_dir: str | None = field(metadata={MetaValues.STREAMABLE: False}, default=None) + check_job_event_data: bool = False + container_auth_data: dict[str, str] | None = None + container_image: str = "" + container_options: list[str] | None = None + container_volume_mounts: list[str] | None = None + container_workdir: str | None = None + envvars: dict[str, Any] | None = None + fact_cache: str | None = field(metadata={MetaValues.STREAMABLE: False}, default=None) + fact_cache_type: str = 'jsonfile' + host_cwd: str | None = None + ident: str | None = field(metadata={MetaValues.STREAMABLE: False}, default=None) + json_mode: bool = False + keepalive_seconds: int | None = field(metadata={MetaValues.STREAMABLE: False}, default=None) + passwords: dict[str, str] | None = None + process_isolation: bool = False + process_isolation_executable: str = defaults.default_process_isolation_executable + project_dir: str | None = field(metadata={MetaValues.STREAMABLE: False}, default=None) + quiet: bool = False + rotate_artifacts: int = 0 + settings: dict | None = None + ssh_key: str | None = None + suppress_env_files: bool = False + timeout: int | None = None + + event_handler: Callable[[dict], None] | None = field(metadata={MetaValues.STREAMABLE: False}, default=None) + status_handler: Callable[[dict, BaseConfig], bool] | None = field(metadata={MetaValues.STREAMABLE: False}, default=None) + artifacts_handler: Callable[[str], None] | None = field(metadata={MetaValues.STREAMABLE: False}, default=None) + cancel_callback: Callable[[], bool] | None = field(metadata={MetaValues.STREAMABLE: False}, default=None) + finished_callback: Callable[[BaseConfig], None] | None = field(metadata={MetaValues.STREAMABLE: False}, default=None) _CONTAINER_ENGINES = ('docker', 'podman') diff --git a/src/ansible_runner/config/runner.py b/src/ansible_runner/config/runner.py index d4c1bcd3..3430bd54 100644 --- a/src/ansible_runner/config/runner.py +++ b/src/ansible_runner/config/runner.py @@ -28,10 +28,11 @@ import tempfile import shutil -from dataclasses import dataclass, field +from dataclasses import dataclass, fields +from typing import Any from ansible_runner import output -from ansible_runner.config._base import BaseConfig, BaseExecutionMode +from ansible_runner.config._base import BaseConfig, BaseExecutionMode, MetaValues from ansible_runner.exceptions import ConfigurationError from ansible_runner.output import debug from ansible_runner.utils import register_for_cleanup @@ -67,32 +68,32 @@ class RunnerConfig(BaseConfig): """ # 'binary' comes from the --binary CLI opt for an alternative ansible command path - binary: str | None = field(metadata={}, default=None) - cmdline: str | None = field(metadata={}, default=None) - directory_isolation_base_path: str | None = field(metadata={}, default=None) - extravars: dict | None = field(metadata={}, default=None) - forks: int | None = field(metadata={}, default=None) - host_pattern: str | None = field(metadata={}, default=None) - inventory: str | dict | list | None = field(metadata={}, default=None) - limit: str | None = field(metadata={}, default=None) - module: str | None = field(metadata={}, default=None) - module_args: str | None = field(metadata={}, default=None) - omit_event_data: bool = field(metadata={}, default=False) - only_failed_event_data: bool = field(metadata={}, default=False) - playbook: str | dict | list | None = field(metadata={}, default=None) - process_isolation_hide_paths: str | list | None = field(metadata={}, default=None) - process_isolation_ro_paths: str | list | None = field(metadata={}, default=None) - process_isolation_show_paths: str | list | None = field(metadata={}, default=None) - process_isolation_path: str | None = field(metadata={}, default=None) + binary: str | None = None + cmdline: str | None = None + directory_isolation_base_path: str | None = None + extravars: dict | None = None + forks: int | None = None + host_pattern: str | None = None + inventory: str | dict | list | None = None + limit: str | None = None + module: str | None = None + module_args: str | None = None + omit_event_data: bool = False + only_failed_event_data: bool = False + playbook: str | dict | list | None = None + process_isolation_hide_paths: str | list | None = None + process_isolation_ro_paths: str | list | None = None + process_isolation_show_paths: str | list | None = None + process_isolation_path: str | None = None role: str = "" role_skip_facts: bool = False - roles_path: str | None = field(metadata={}, default=None) + roles_path: str | None = None role_vars: dict[str, str] | None = None - skip_tags: str | None = field(metadata={}, default=None) - suppress_ansible_output: bool = field(metadata={}, default=False) - suppress_output_file: bool = field(metadata={}, default=False) - tags: str | None = field(metadata={}, default=None) - verbosity: int | None = field(metadata={}, default=None) + skip_tags: str | None = None + suppress_ansible_output: bool = False + suppress_output_file: bool = False + tags: str | None = None + verbosity: int | None = None def __post_init__(self) -> None: # NOTE: Cannot call base class __init__() here as that causes some recursion madness. @@ -148,6 +149,23 @@ def extra_vars(self): def extra_vars(self, value): self.extravars = value + def streamable_attributes(self) -> dict[str, Any]: + """Get the set of streamable attributes that have a value that is different from the default. + + The field metadata indicates if the attribute is streamable. By default, an attribute + is considered streamable (must be explicitly disabled). + + :return: A dict of attribute names and their values. + """ + retval = {} + for field_obj in fields(self): + if field_obj.metadata and not field_obj.metadata.get(MetaValues.STREAMABLE, True): + continue + current_value = getattr(self, field_obj.name) + if not field_obj.default == current_value: + retval[field_obj.name] = current_value + return retval + def prepare(self): """ Performs basic checks and then properly invokes diff --git a/test/unit/config/test_runner.py b/test/unit/config/test_runner.py index 6100d3b7..8b060e92 100644 --- a/test/unit/config/test_runner.py +++ b/test/unit/config/test_runner.py @@ -735,3 +735,25 @@ def test_containerization_settings(tmp_path, runtime, mocker): ['my_container', 'ansible-playbook', '-i', '/runner/inventory', 'main.yaml'] assert expected_command_start == rc.command + + +def test_streamable_attributes_all_defaults(): + """Test that all default values return an empty dict.""" + rc = RunnerConfig() + assert not rc.streamable_attributes() + + +def test_streamable_attributes_non_default(tmp_path): + """Test that non-default, streamable values are returned.""" + rc = RunnerConfig(private_data_dir=str(tmp_path), + keepalive_seconds=10, + host_pattern="hostA,", + json_mode=True, + verbosity=3) + + # Don't expect private_data_dir or keepalive_seconds since they are not streamable. + assert rc.streamable_attributes() == { + "host_pattern": "hostA,", + "json_mode": True, + "verbosity": 3, + }