feat(validation): treat any <placeholder> sub-option as a keyed collection

This commit is contained in:
2026-10-05 18:21:02 +02:00
parent fec3fbfa8e
commit 7129ceb515
2 changed files with 6 additions and 6 deletions
+5 -5
View File
@@ -2,8 +2,8 @@
Each spec file's ``argument_specs.inputs.options`` declares top-level variables. Every one the host Each spec file's ``argument_specs.inputs.options`` declares top-level variables. Every one the host
defines is checked without Ansible's lenient coercion: a string is never a list, bool or dict. A dict defines is checked without Ansible's lenient coercion: a string is never a list, bool or dict. A dict
option whose only sub-option is named ``<name>`` is a collection keyed by name; each entry is checked option whose only sub-option is a placeholder such as ``<name>`` is a collection keyed by name; each
against that sub-option, and a null entry means the entry is not managed. Messages name the key path entry is checked against that sub-option, and a null entry means the entry is not managed. Messages name the key path
and the types involved but never a value, so secrets cannot leak into job output. and the types involved but never a value, so secrets cannot leak into job output.
""" """
@@ -14,7 +14,6 @@ from ansible.errors import AnsibleError
from ansible.parsing.vault import EncryptedString from ansible.parsing.vault import EncryptedString
from ansible.plugins.action import ActionBase from ansible.plugins.action import ActionBase
KEYED = "<name>"
SPEC_KEYS = {"type", "required", "default", "choices", "elements", "options", "description", "no_log"} SPEC_KEYS = {"type", "required", "default", "choices", "elements", "options", "description", "no_log"}
UNRESOLVED = object() UNRESOLVED = object()
@@ -91,10 +90,11 @@ class _Validator:
self.check(element, item, f"{path}[{index}]") self.check(element, item, f"{path}[{index}]")
def check_options(self, options, value, path): def check_options(self, options, value, path):
if set(options) == {KEYED}: if len(options) == 1 and next(iter(options)).startswith("<"):
entry_spec = next(iter(options.values()))
for name, entry in value.items(): for name, entry in value.items():
if entry is not None: if entry is not None:
self.check(options[KEYED], entry, f"{path}.{name}") self.check(entry_spec, entry, f"{path}.{name}")
return return
for key in value: for key in value:
if key not in options: if key not in options:
+1 -1
View File
@@ -36,7 +36,7 @@ argument_specs:
users: users:
type: dict type: dict
options: options:
"<name>": "<user>":
type: dict type: dict
options: options:
password: password: