diff --git a/plugins/action/validate_inputs.py b/plugins/action/validate_inputs.py index 309a512..dccc060 100644 --- a/plugins/action/validate_inputs.py +++ b/plugins/action/validate_inputs.py @@ -7,6 +7,7 @@ against that sub-option, and a null entry means the entry is not managed. Messag and the types involved but never a value, so secrets cannot leak into job output. """ +import glob from collections.abc import Mapping from ansible.errors import AnsibleError @@ -111,7 +112,15 @@ class ActionModule(ActionBase): task_vars = task_vars or {} validator = _Validator(self._templar) declared = {} - for path in self._task.args["specs"]: + paths = [] + # A pattern (`.../*/roles/*/meta/argument_specs.yml`) picks up every role's spec without a list + # to keep in sync; one that matches nothing is a broken path, not an empty set. + for entry in self._task.args["specs"]: + matches = sorted(glob.glob(entry)) if glob.has_magic(entry) else [entry] + if not matches: + raise AnsibleError(f"no spec file matches {entry}") + paths.extend(matches) + for path in paths: options = self._loader.load_from_file(path)["argument_specs"]["inputs"]["options"] _check_spec(options, path) for name, spec in options.items(): diff --git a/tests/inputs/test_inputs.yml b/tests/inputs/test_inputs.yml index 1663b34..a8bf821 100644 --- a/tests/inputs/test_inputs.yml +++ b/tests/inputs/test_inputs.yml @@ -14,6 +14,41 @@ loop_var: inf label: "{{ inf.name }}" + - name: Expand a spec pattern + validate_inputs: + specs: + - "{{ playbook_dir }}/fixture_*.yml" + vars: + fixture: + name: web01 + servers: [10.0.0.1] + register: inputs_pattern_result + + - name: Assert the pattern matched the fixture spec + ansible.builtin.assert: + that: + - inputs_pattern_result is succeeded + success_msg: "[inputs pattern] OK" + + - name: Refuse a pattern that matches nothing + block: + - name: Run the validator with a pattern that matches no file + validate_inputs: + specs: + - "{{ playbook_dir }}/nothing_*.yml" + register: inputs_nomatch_result + rescue: + - name: Keep the no-match failure + ansible.builtin.set_fact: + inputs_nomatch_result: "{{ ansible_failed_result }}" + + - name: Assert the empty pattern was refused + ansible.builtin.assert: + that: + - inputs_nomatch_result.msg is search('no spec file matches') + fail_msg: "[inputs no-match] got {{ inputs_nomatch_result.msg | default('') }}" + success_msg: "[inputs no-match] OK" + - name: Refuse a variable declared by two specs block: - name: Run the validator with the fixture spec twice