diff --git a/changelogs/fragments/j2_load_fix.yml b/changelogs/fragments/j2_load_fix.yml new file mode 100644 index 00000000000..4fd33b964a8 --- /dev/null +++ b/changelogs/fragments/j2_load_fix.yml @@ -0,0 +1,3 @@ +bugfixes: + - Plugin loader does not dedupe nor cache filter/test plugins by file basename, but full path name. + - Restoring the ability of filters/tests can have same file base name but different tests/filters defined inside. diff --git a/lib/ansible/plugins/loader.py b/lib/ansible/plugins/loader.py index 9e8213518ed..9c1682001b0 100644 --- a/lib/ansible/plugins/loader.py +++ b/lib/ansible/plugins/loader.py @@ -1005,23 +1005,32 @@ class PluginLoader: loaded_modules = set() for path in all_matches: + name = os.path.splitext(path)[0] basename = os.path.basename(name) + is_j2 = isinstance(self, Jinja2Loader) + + if is_j2: + ref_name = path + else: + ref_name = basename - if basename in _PLUGIN_FILTERS[self.package]: + if not is_j2 and basename in _PLUGIN_FILTERS[self.package]: + # j2 plugins get processed in own class, here they would just be container files display.debug("'%s' skipped due to a defined plugin filter" % basename) continue if basename == '__init__' or (basename == 'base' and self.package == 'ansible.plugins.cache'): # cache has legacy 'base.py' file, which is wrapper for __init__.py - display.debug("'%s' skipped due to reserved name" % basename) + display.debug("'%s' skipped due to reserved name" % name) continue - if dedupe and basename in loaded_modules: - display.debug("'%s' skipped as duplicate" % basename) + if dedupe and ref_name in loaded_modules: + # for j2 this is 'same file', other plugins it is basename + display.debug("'%s' skipped as duplicate" % ref_name) continue - loaded_modules.add(basename) + loaded_modules.add(ref_name) if path_only: yield path diff --git a/test/integration/targets/plugin_loader/file_collision/play.yml b/test/integration/targets/plugin_loader/file_collision/play.yml new file mode 100644 index 00000000000..cc55800c527 --- /dev/null +++ b/test/integration/targets/plugin_loader/file_collision/play.yml @@ -0,0 +1,7 @@ +- hosts: localhost + gather_facts: false + roles: + - r1 + - r2 + tasks: + - debug: msg={{'a'|filter1|filter2|filter3}} diff --git a/test/integration/targets/plugin_loader/file_collision/roles/r1/filter_plugins/custom.py b/test/integration/targets/plugin_loader/file_collision/roles/r1/filter_plugins/custom.py new file mode 100644 index 00000000000..7adbf7dcede --- /dev/null +++ b/test/integration/targets/plugin_loader/file_collision/roles/r1/filter_plugins/custom.py @@ -0,0 +1,15 @@ +from __future__ import annotations + + +def do_nothing(myval): + return myval + + +class FilterModule(object): + ''' Ansible core jinja2 filters ''' + + def filters(self): + return { + 'filter1': do_nothing, + 'filter3': do_nothing, + } diff --git a/test/integration/targets/plugin_loader/file_collision/roles/r1/filter_plugins/filter1.yml b/test/integration/targets/plugin_loader/file_collision/roles/r1/filter_plugins/filter1.yml new file mode 100644 index 00000000000..5bb3e345bef --- /dev/null +++ b/test/integration/targets/plugin_loader/file_collision/roles/r1/filter_plugins/filter1.yml @@ -0,0 +1,18 @@ +DOCUMENTATION: + name: filter1 + version_added: "1.9" + short_description: Does nothing + description: + - Really, does nothing + notes: + - This is a test filter + positional: _input + options: + _input: + description: the input + required: true + +EXAMPLES: '' +RETURN: + _value: + description: The input diff --git a/test/integration/targets/plugin_loader/file_collision/roles/r1/filter_plugins/filter3.yml b/test/integration/targets/plugin_loader/file_collision/roles/r1/filter_plugins/filter3.yml new file mode 100644 index 00000000000..4270b32c485 --- /dev/null +++ b/test/integration/targets/plugin_loader/file_collision/roles/r1/filter_plugins/filter3.yml @@ -0,0 +1,18 @@ +DOCUMENTATION: + name: filter3 + version_added: "1.9" + short_description: Does nothing + description: + - Really, does nothing + notes: + - This is a test filter + positional: _input + options: + _input: + description: the input + required: true + +EXAMPLES: '' +RETURN: + _value: + description: The input diff --git a/test/integration/targets/plugin_loader/file_collision/roles/r2/filter_plugins/custom.py b/test/integration/targets/plugin_loader/file_collision/roles/r2/filter_plugins/custom.py new file mode 100644 index 00000000000..8a7a4f5270d --- /dev/null +++ b/test/integration/targets/plugin_loader/file_collision/roles/r2/filter_plugins/custom.py @@ -0,0 +1,14 @@ +from __future__ import annotations + + +def do_nothing(myval): + return myval + + +class FilterModule(object): + ''' Ansible core jinja2 filters ''' + + def filters(self): + return { + 'filter2': do_nothing, + } diff --git a/test/integration/targets/plugin_loader/file_collision/roles/r2/filter_plugins/filter2.yml b/test/integration/targets/plugin_loader/file_collision/roles/r2/filter_plugins/filter2.yml new file mode 100644 index 00000000000..de9195e66b7 --- /dev/null +++ b/test/integration/targets/plugin_loader/file_collision/roles/r2/filter_plugins/filter2.yml @@ -0,0 +1,18 @@ +DOCUMENTATION: + name: filter2 + version_added: "1.9" + short_description: Does nothing + description: + - Really, does nothing + notes: + - This is a test filter + positional: _input + options: + _input: + description: the input + required: true + +EXAMPLES: '' +RETURN: + _value: + description: The input diff --git a/test/integration/targets/plugin_loader/runme.sh b/test/integration/targets/plugin_loader/runme.sh index e30f62419b3..092108ff8aa 100755 --- a/test/integration/targets/plugin_loader/runme.sh +++ b/test/integration/targets/plugin_loader/runme.sh @@ -34,3 +34,6 @@ done # test config loading ansible-playbook use_coll_name.yml -i ../../inventory -e 'ansible_connection=ansible.builtin.ssh' "$@" + +# test filter loading ignoring duplicate file basename +ansible-playbook file_collision/play.yml "$@"