diff --git a/changelogs/fragments/template-tags-on-play-roles.yml b/changelogs/fragments/template-tags-on-play-roles.yml new file mode 100644 index 00000000000..8954ba1b14b --- /dev/null +++ b/changelogs/fragments/template-tags-on-play-roles.yml @@ -0,0 +1,2 @@ +bugfixes: + - Fix templating ``tags`` on plays and roles. (https://github.com/ansible/ansible/issues/69903) diff --git a/lib/ansible/playbook/taggable.py b/lib/ansible/playbook/taggable.py index db56225bd1a..64d8494b945 100644 --- a/lib/ansible/playbook/taggable.py +++ b/lib/ansible/playbook/taggable.py @@ -17,6 +17,8 @@ from __future__ import annotations +import typing as t + from ansible.errors import AnsibleError from ansible.module_utils.six import string_types from ansible.module_utils.common.sentinel import Sentinel @@ -25,7 +27,7 @@ from ansible.playbook.attribute import FieldAttribute from ansible._internal._templating._engine import TemplateEngine -def _flatten_tags(tags: list) -> list: +def _flatten_tags(tags: list[str | int]) -> list[str | int]: rv = set() for tag in tags: if isinstance(tag, list): @@ -49,16 +51,28 @@ class Taggable: raise AnsibleError('tags must be specified as a list', obj=ds) + def _get_all_taggable_objects(self) -> t.Iterable[Taggable]: + obj = self + while obj is not None: + yield obj + + if (role := getattr(obj, "_role", Sentinel)) is not Sentinel: + yield role # type: ignore[misc] + + obj = obj._parent + + yield self.get_play() + def evaluate_tags(self, only_tags, skip_tags, all_vars): - """ this checks if the current item should be executed depending on tag options """ + """Check if the current item should be executed depending on the specified tags. + NOTE this method is assumed to be called only on Task objects. + """ if self.tags: templar = TemplateEngine(loader=self._loader, variables=all_vars) - obj = self - while obj is not None: + for obj in self._get_all_taggable_objects(): if (_tags := getattr(obj, "_tags", Sentinel)) is not Sentinel: obj._tags = _flatten_tags(templar.template(_tags)) - obj = obj._parent tags = set(self.tags) else: # this makes isdisjoint work for untagged diff --git a/test/integration/targets/tags/roles/a/tasks/main.yml b/test/integration/targets/tags/roles/a/tasks/main.yml new file mode 100644 index 00000000000..86c04d61fa8 --- /dev/null +++ b/test/integration/targets/tags/roles/a/tasks/main.yml @@ -0,0 +1,2 @@ +- import_role: + name: b diff --git a/test/integration/targets/tags/roles/b/tasks/main.yml b/test/integration/targets/tags/roles/b/tasks/main.yml new file mode 100644 index 00000000000..ccff84f943d --- /dev/null +++ b/test/integration/targets/tags/roles/b/tasks/main.yml @@ -0,0 +1,2 @@ +- debug: + msg: Tagged_task diff --git a/test/integration/targets/tags/runme.sh b/test/integration/targets/tags/runme.sh index 7dcb998560b..3e0828f5d2c 100755 --- a/test/integration/targets/tags/runme.sh +++ b/test/integration/targets/tags/runme.sh @@ -82,3 +82,21 @@ ansible-playbook test_template_parent_tags.yml --tags tag1 "$@" 2>&1 | tee out.t ansible-playbook test_template_parent_tags.yml --skip-tags tag1 "$@" 2>&1 | tee out.txt [ "$(grep out.txt -ce 'Tagged_task')" = "0" ]; rm out.txt + +ansible-playbook test_template_play_tags.yml "$@" 2>&1 | tee out.txt +[ "$(grep out.txt -ce 'Tagged_task')" = "1" ]; rm out.txt + +ansible-playbook test_template_play_tags.yml --tags tag1 "$@" 2>&1 | tee out.txt +[ "$(grep out.txt -ce 'Tagged_task')" = "1" ]; rm out.txt + +ansible-playbook test_template_play_tags.yml --skip-tags tag1 "$@" 2>&1 | tee out.txt +[ "$(grep out.txt -ce 'Tagged_task')" = "0" ]; rm out.txt + +ansible-playbook test_template_role_tags.yml "$@" 2>&1 | tee out.txt +[ "$(grep out.txt -ce 'Tagged_task')" = "1" ]; rm out.txt + +ansible-playbook test_template_role_tags.yml --tags tag1 "$@" 2>&1 | tee out.txt +[ "$(grep out.txt -ce 'Tagged_task')" = "1" ]; rm out.txt + +ansible-playbook test_template_role_tags.yml --skip-tags tag1 "$@" 2>&1 | tee out.txt +[ "$(grep out.txt -ce 'Tagged_task')" = "0" ]; rm out.txt diff --git a/test/integration/targets/tags/test_template_play_tags.yml b/test/integration/targets/tags/test_template_play_tags.yml new file mode 100644 index 00000000000..f6bc2d40299 --- /dev/null +++ b/test/integration/targets/tags/test_template_play_tags.yml @@ -0,0 +1,8 @@ +- hosts: localhost + gather_facts: false + vars: + t: tag1 + tags: "{{ t }}" + tasks: + - debug: + msg: Tagged_task diff --git a/test/integration/targets/tags/test_template_role_tags.yml b/test/integration/targets/tags/test_template_role_tags.yml new file mode 100644 index 00000000000..386255daad8 --- /dev/null +++ b/test/integration/targets/tags/test_template_role_tags.yml @@ -0,0 +1,7 @@ +- hosts: localhost + gather_facts: false + vars: + t: tag1 + roles: + - name: a + tags: "{{ t }}" diff --git a/test/units/playbook/test_taggable.py b/test/units/playbook/test_taggable.py index 7076f98066d..fe713ba9533 100644 --- a/test/units/playbook/test_taggable.py +++ b/test/units/playbook/test_taggable.py @@ -29,6 +29,9 @@ class TaggableTestObj(Taggable): self.tags = [] self._parent = None + def get_play(self): + return None + class TestTaggable(unittest.TestCase):