diff --git a/changelogs/fragments/1312-swarm-secrets.yml b/changelogs/fragments/1312-swarm-secrets.yml new file mode 100644 index 00000000..02196c41 --- /dev/null +++ b/changelogs/fragments/1312-swarm-secrets.yml @@ -0,0 +1,2 @@ +minor_changes: + - "docker_swarm - on ansible-core 2.22+, the ``Manager`` and ``Worker`` join tokens and ``UnlockKey`` are marked as secrets (https://github.com/ansible-collections/community.docker/pull/1312)." diff --git a/plugins/module_utils/_secrets.py b/plugins/module_utils/_secrets.py new file mode 100644 index 00000000..ae068ebc --- /dev/null +++ b/plugins/module_utils/_secrets.py @@ -0,0 +1,49 @@ +# Copyright (c) 2026 Felix Fontein +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +import typing as t +from collections.abc import Mapping, Sequence + +try: + from ansible.module_utils.secrets import register_secret, register_secrets # type: ignore[import-not-found] + + HAS_SECRETS_API = True +except ImportError: + HAS_SECRETS_API = False + +_T = t.TypeVar("_T") + + +def _collect_recursively( + value: t.Any, collected_values: list[str], *, int_to_string: bool = False +) -> None: + if isinstance(value, Mapping): + for v in value.items(): + _collect_recursively(v, collected_values, int_to_string=int_to_string) + elif isinstance(value, str): + collected_values.append(value) + elif isinstance(value, Sequence): + for v in value: + _collect_recursively(v, collected_values, int_to_string=int_to_string) + elif int_to_string and isinstance(value, int) and not isinstance(value, bool): + collected_values.append(str(value)) + + +def mark_values_as_secrets(value: _T, *, int_to_string: bool = False) -> _T: + """Register all strings appearing in the (potentially nested) data structure ``value`` as secrets.""" + if HAS_SECRETS_API: + collected_values: list[str] = [] + _collect_recursively(value, collected_values, int_to_string=int_to_string) + if collected_values: + register_secrets(collected_values) + return value + + +def mark_as_secret(value: str) -> str: + """Register a string as a secret.""" + if HAS_SECRETS_API: + value = register_secret(value) + return value diff --git a/plugins/modules/docker_swarm.py b/plugins/modules/docker_swarm.py index 65aca598..e41e7c0c 100644 --- a/plugins/modules/docker_swarm.py +++ b/plugins/modules/docker_swarm.py @@ -262,21 +262,29 @@ swarm_facts: Worker: description: - Token to join the cluster as a new *worker* node. - - B(Note:) if this value has been specified as O(join_token), the value here will not be the token, but C(VALUE_SPECIFIED_IN_NO_LOG_PARAMETER). + - B(Note:) for ansible-core before 2.22, if this value has been specified as O(join_token), + the value here will not be the token, but C(VALUE_SPECIFIED_IN_NO_LOG_PARAMETER). If you pass O(join_token), make sure your playbook/role does not depend on this return value! + For ansible-core 2.22+, this value is always properly returned. It will be registered as a secret, + though, and thus cannot be shown with M(ansible.builtin.debug). returned: success type: str example: SWMTKN-1--xxxxx Manager: description: - Token to join the cluster as a new *manager* node. - - B(Note:) if this value has been specified as O(join_token), the value here will not be the token, but C(VALUE_SPECIFIED_IN_NO_LOG_PARAMETER). + - B(Note:) for ansible-core before 2.22, if this value has been specified as O(join_token), + the value here will not be the token, but C(VALUE_SPECIFIED_IN_NO_LOG_PARAMETER). If you pass O(join_token), make sure your playbook/role does not depend on this return value! + For ansible-core 2.22+, this value is always properly returned. It will be registered as a secret, + though, and thus cannot be shown with M(ansible.builtin.debug). returned: success type: str example: SWMTKN-1--xxxxx UnlockKey: - description: The swarm unlock-key if O(autolock_managers=true). + description: + - The swarm unlock-key if O(autolock_managers=true). + - B(Note) that on ansible-core 2.22+, this will be registered as a secret. returned: on success if O(autolock_managers=true) and swarm is initialised, or if O(autolock_managers) has changed. type: str example: SWMKEY-1-xxx @@ -302,6 +310,9 @@ except ImportError: from ansible_collections.community.docker.plugins.module_utils._common import ( RequestException, ) +from ansible_collections.community.docker.plugins.module_utils._secrets import ( + mark_values_as_secrets, +) from ansible_collections.community.docker.plugins.module_utils._swarm import ( AnsibleDockerSwarmClient, ) @@ -563,8 +574,8 @@ class SwarmManager(DockerBaseClass): self.differences.add("state", parameter="present", active="absent") self.results["changed"] = True self.results["swarm_facts"] = { - "JoinTokens": self.swarm_info.get("JoinTokens"), - "UnlockKey": self.swarm_info.get("UnlockKey"), + "JoinTokens": mark_values_as_secrets(self.swarm_info.get("JoinTokens")), + "UnlockKey": mark_values_as_secrets(self.swarm_info.get("UnlockKey")), } def __update_swarm(self) -> None: