diff --git a/changelogs/fragments/include_tasks_parent_templating.yml b/changelogs/fragments/include_tasks_parent_templating.yml new file mode 100644 index 00000000000..0e227974aa4 --- /dev/null +++ b/changelogs/fragments/include_tasks_parent_templating.yml @@ -0,0 +1,3 @@ +bugfixes: +- include_tasks - Ensure we give IncludedFile the same context as TaskExecutor when templating the parent include path + allowing for lookups in the included file path (https://github.com/ansible/ansible/issues/49969) diff --git a/lib/ansible/playbook/included_file.py b/lib/ansible/playbook/included_file.py index 51649812dea..6f0b0bfc72d 100644 --- a/lib/ansible/playbook/included_file.py +++ b/lib/ansible/playbook/included_file.py @@ -21,6 +21,8 @@ __metaclass__ = type import os +from ansible.errors import AnsibleError +from ansible.module_utils._text import to_text from ansible.playbook.task_include import TaskInclude from ansible.playbook.role_include import IncludeRole from ansible.template import Templar @@ -78,7 +80,6 @@ class IncludedFile: task_vars = task_vars_cache[cache_key] except KeyError: task_vars = task_vars_cache[cache_key] = variable_manager.get_vars(play=iterator._play, host=original_host, task=original_task) - templar = Templar(loader=loader, variables=task_vars) include_variables = include_result.get('include_variables', dict()) loop_var = 'item' @@ -95,6 +96,16 @@ class IncludedFile: if original_task.no_log and '_ansible_no_log' not in include_variables: task_vars['_ansible_no_log'] = include_variables['_ansible_no_log'] = original_task.no_log + # get search path for this task to pass to lookup plugins that may be used in pathing to + # the included file + task_vars['ansible_search_path'] = original_task.get_search_path() + + # ensure basedir is always in (dwim already searches here but we need to display it) + if loader.get_basedir() not in task_vars['ansible_search_path']: + task_vars['ansible_search_path'].append(loader.get_basedir()) + + templar = Templar(loader=loader, variables=task_vars) + if original_task.action in ('include', 'include_tasks'): include_file = None if original_task: @@ -113,7 +124,15 @@ class IncludedFile: if isinstance(parent_include, IncludeRole): parent_include_dir = parent_include._role_path else: - parent_include_dir = os.path.dirname(templar.template(parent_include.args.get('_raw_params'))) + try: + parent_include_dir = os.path.dirname(templar.template(parent_include.args.get('_raw_params'))) + except AnsibleError as e: + parent_include_dir = '' + display.warning( + 'Templating the path of the parent %s failed. The path to the ' + 'included file may not be found. ' + 'The error was: %s.' % (original_task.action, to_text(e)) + ) if cumulative_path is not None and not os.path.isabs(cumulative_path): cumulative_path = os.path.join(parent_include_dir, cumulative_path) else: diff --git a/test/integration/targets/include_import/parent_templating/playbook.yml b/test/integration/targets/include_import/parent_templating/playbook.yml new file mode 100644 index 00000000000..b7330206e46 --- /dev/null +++ b/test/integration/targets/include_import/parent_templating/playbook.yml @@ -0,0 +1,11 @@ +# https://github.com/ansible/ansible/issues/49969 +- hosts: localhost + gather_facts: false + tasks: + - include_role: + name: test + public: true + + - assert: + that: + - included_other is defined diff --git a/test/integration/targets/include_import/parent_templating/roles/test/tasks/localhost.yml b/test/integration/targets/include_import/parent_templating/roles/test/tasks/localhost.yml new file mode 100644 index 00000000000..e5b281e7acc --- /dev/null +++ b/test/integration/targets/include_import/parent_templating/roles/test/tasks/localhost.yml @@ -0,0 +1 @@ +- include_tasks: other.yml diff --git a/test/integration/targets/include_import/parent_templating/roles/test/tasks/main.yml b/test/integration/targets/include_import/parent_templating/roles/test/tasks/main.yml new file mode 100644 index 00000000000..16fba69ab03 --- /dev/null +++ b/test/integration/targets/include_import/parent_templating/roles/test/tasks/main.yml @@ -0,0 +1 @@ +- include_tasks: "{{ lookup('first_found', inventory_hostname ~ '.yml') }}" diff --git a/test/integration/targets/include_import/parent_templating/roles/test/tasks/other.yml b/test/integration/targets/include_import/parent_templating/roles/test/tasks/other.yml new file mode 100644 index 00000000000..c3bae1a5d45 --- /dev/null +++ b/test/integration/targets/include_import/parent_templating/roles/test/tasks/other.yml @@ -0,0 +1,2 @@ +- set_fact: + included_other: true diff --git a/test/integration/targets/include_import/runme.sh b/test/integration/targets/include_import/runme.sh index 4fc2d9a538c..d2fe403cd95 100755 --- a/test/integration/targets/include_import/runme.sh +++ b/test/integration/targets/include_import/runme.sh @@ -90,3 +90,7 @@ ansible-playbook run_once/playbook.yml "$@" # https://github.com/ansible/ansible/issues/48936 ansible-playbook -v handler_addressing/playbook.yml 2>&1 | tee test_handler_addressing.out test "$(egrep -c 'include handler task|ERROR! The requested handler '"'"'do_import'"'"' was not found' test_handler_addressing.out)" = 2 + +# https://github.com/ansible/ansible/issues/49969 +ansible-playbook -v parent_templating/playbook.yml 2>&1 | tee test_parent_templating.out +test "$(egrep -c 'Templating the path of the parent include_tasks failed.' test_parent_templating.out)" = 0 diff --git a/test/units/playbook/test_included_file.py b/test/units/playbook/test_included_file.py index e4f1b3bd180..dc919cbef19 100644 --- a/test/units/playbook/test_included_file.py +++ b/test/units/playbook/test_included_file.py @@ -65,6 +65,7 @@ def test_process_include_results(mock_iterator, mock_variable_manager): parent_task_ds = {'debug': 'msg=foo'} parent_task = Task.load(parent_task_ds) + parent_task._play = None task_ds = {'include': 'include_test.yml'} loaded_task = TaskInclude.load(task_ds, task_include=parent_task) @@ -91,12 +92,15 @@ def test_process_include_diff_files(mock_iterator, mock_variable_manager): parent_task_ds = {'debug': 'msg=foo'} parent_task = Task.load(parent_task_ds) + parent_task._play = None task_ds = {'include': 'include_test.yml'} loaded_task = TaskInclude.load(task_ds, task_include=parent_task) + loaded_task._play = None child_task_ds = {'include': 'other_include_test.yml'} loaded_child_task = TaskInclude.load(child_task_ds, task_include=loaded_task) + loaded_child_task._play = None return_data = {'include': 'include_test.yml'} # The task in the TaskResult has to be a TaskInclude so it has a .static attr @@ -129,6 +133,9 @@ def test_process_include_simulate_free(mock_iterator, mock_variable_manager): parent_task1 = Task.load(parent_task_ds) parent_task2 = Task.load(parent_task_ds) + parent_task1._play = None + parent_task2._play = None + task_ds = {'include': 'include_test.yml'} loaded_task1 = TaskInclude.load(task_ds, task_include=parent_task1) loaded_task2 = TaskInclude.load(task_ds, task_include=parent_task2)