Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix loading vars_plugins in roles #82273

Merged
merged 6 commits into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelogs/fragments/fix-vars-plugins-in-roles.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bugfixes:
- Fix loading vars_plugins in roles (https://github.com/ansible/ansible/issues/82239).
39 changes: 23 additions & 16 deletions lib/ansible/plugins/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,7 @@ def __init__(self, class_name, package, config, subdir, aliases=None, required_b
self._module_cache = MODULE_CACHE[class_name]
self._paths = PATH_CACHE[class_name]
self._plugin_path_cache = PLUGIN_PATH_CACHE[class_name]
try:
self._plugin_instance_cache = {} if self.type == 'vars' else None
except ValueError:
self._plugin_instance_cache = None
self._plugin_instance_cache = {} if self.subdir == 'vars_plugins' else None

self._searched_paths = set()

Expand All @@ -265,7 +262,7 @@ def _clear_caches(self):
self._module_cache = MODULE_CACHE[self.class_name]
self._paths = PATH_CACHE[self.class_name]
self._plugin_path_cache = PLUGIN_PATH_CACHE[self.class_name]
self._plugin_instance_cache = {} if self.type == 'vars' else None
self._plugin_instance_cache = {} if self.subdir == 'vars_plugins' else None
self._searched_paths = set()

def __setstate__(self, data):
Expand Down Expand Up @@ -871,12 +868,12 @@ def get_with_context(self, name, *args, **kwargs):
if name in self.aliases:
name = self.aliases[name]

if self._plugin_instance_cache and (cached_load_result := self._plugin_instance_cache.get(name)):
if (cached_result := (self._plugin_instance_cache or {}).get(name)) and cached_result[1].resolved:
# Resolving the FQCN is slow, even if we've passed in the resolved FQCN.
# Short-circuit here if we've previously resolved this name.
# This will need to be restricted if non-vars plugins start using the cache, since
# some non-fqcn plugin need to be resolved again with the collections list.
return get_with_context_result(*cached_load_result)
return get_with_context_result(*cached_result)

plugin_load_context = self.find_plugin_with_context(name, collection_list=collection_list)
if not plugin_load_context.resolved or not plugin_load_context.plugin_resolved_path:
Expand All @@ -888,10 +885,10 @@ def get_with_context(self, name, *args, **kwargs):
fq_name = '.'.join((plugin_load_context.plugin_resolved_collection, fq_name))
resolved_type_name = plugin_load_context.plugin_resolved_name
path = plugin_load_context.plugin_resolved_path
if self._plugin_instance_cache and (cached_load_result := self._plugin_instance_cache.get(fq_name)):
if (cached_result := (self._plugin_instance_cache or {}).get(fq_name)) and cached_result[1].resolved:
# This is unused by vars plugins, but it's here in case the instance cache expands to other plugin types.
# We get here if we've seen this plugin before, but it wasn't called with the resolved FQCN.
return get_with_context_result(*cached_load_result)
return get_with_context_result(*cached_result)
redirected_names = plugin_load_context.redirect_list or []

if path not in self._module_cache:
Expand Down Expand Up @@ -934,8 +931,10 @@ def get_with_context(self, name, *args, **kwargs):

self._update_object(obj, resolved_type_name, path, redirected_names, fq_name)
if self._plugin_instance_cache is not None and getattr(obj, 'is_stateless', False):
# store under both the originally requested name and the resolved FQ name
self._plugin_instance_cache[name] = self._plugin_instance_cache[fq_name] = (obj, plugin_load_context)
self._plugin_instance_cache[fq_name] = (obj, plugin_load_context)
elif self._plugin_instance_cache is not None:
# The cache doubles as the load order, so record the FQCN even if the plugin hasn't set is_stateless = True
self._plugin_instance_cache[fq_name] = (None, PluginLoadContext())
return get_with_context_result(obj, plugin_load_context)

def _display_plugin_load(self, class_name, name, searched_paths, path, found_in_cache=None, class_only=None):
Expand Down Expand Up @@ -1041,9 +1040,9 @@ def all(self, *args, **kwargs):
else:
fqcn = f"ansible.builtin.{basename}"

if self._plugin_instance_cache is not None and fqcn in self._plugin_instance_cache:
if (cached_result := (self._plugin_instance_cache or {}).get(fqcn)) and cached_result[1].resolved:
# Here just in case, but we don't call all() multiple times for vars plugins, so this should not be used.
yield self._plugin_instance_cache[basename][0]
yield cached_result[0]
continue

if path not in self._module_cache:
Expand Down Expand Up @@ -1095,9 +1094,17 @@ def all(self, *args, **kwargs):

self._update_object(obj, basename, path, resolved=fqcn)

if self._plugin_instance_cache is not None and fqcn not in self._plugin_instance_cache:
# Use get_with_context to cache the plugin the first time we see it.
self.get_with_context(fqcn)[0]
if self._plugin_instance_cache is not None:
needs_enabled = False
if hasattr(obj, 'REQUIRES_ENABLED'):
needs_enabled = obj.REQUIRES_ENABLED
elif hasattr(obj, 'REQUIRES_WHITELIST'):
needs_enabled = obj.REQUIRES_WHITELIST
display.deprecated("The VarsModule class variable 'REQUIRES_WHITELIST' is deprecated. "
"Use 'REQUIRES_ENABLED' instead.", version=2.18)
if not needs_enabled:
# Use get_with_context to cache the plugin the first time we see it.
self.get_with_context(fqcn)[0]

yield obj

Expand Down
54 changes: 17 additions & 37 deletions lib/ansible/vars/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,41 +16,14 @@

display = Display()

cached_vars_plugin_order = None


def _load_vars_plugins_order():
def _prime_vars_loader():
# find 3rd party legacy vars plugins once, and look them up by name subsequently
auto = []
for auto_run_plugin in vars_loader.all(class_only=True):
needs_enabled = False
if hasattr(auto_run_plugin, 'REQUIRES_ENABLED'):
needs_enabled = auto_run_plugin.REQUIRES_ENABLED
elif hasattr(auto_run_plugin, 'REQUIRES_WHITELIST'):
needs_enabled = auto_run_plugin.REQUIRES_WHITELIST
display.deprecated("The VarsModule class variable 'REQUIRES_WHITELIST' is deprecated. "
"Use 'REQUIRES_ENABLED' instead.", version=2.18)
if needs_enabled:
continue
auto.append(auto_run_plugin._load_name)

# find enabled plugins once so we can look them up by resolved fqcn subsequently
enabled = []
list(vars_loader.all(class_only=True))
for plugin_name in C.VARIABLE_PLUGINS_ENABLED:
if (plugin := vars_loader.get(plugin_name)) is None:
enabled.append(plugin_name)
else:
collection = '.' in plugin.ansible_name and not plugin.ansible_name.startswith('ansible.builtin.')
# Warn if a collection plugin has REQUIRES_ENABLED because it has no effect.
if collection and (hasattr(plugin, 'REQUIRES_ENABLED') or hasattr(plugin, 'REQUIRES_WHITELIST')):
display.warning(
"Vars plugins in collections must be enabled to be loaded, REQUIRES_ENABLED is not supported. "
"This should be removed from the plugin %s." % plugin.ansible_name
)
enabled.append(plugin.ansible_name)

global cached_vars_plugin_order
cached_vars_plugin_order = auto + enabled
if not plugin_name:
continue
vars_loader.get(plugin_name)


def get_plugin_vars(loader, plugin, path, entities):
Expand Down Expand Up @@ -106,16 +79,23 @@ def _plugin_should_run(plugin, stage):


def get_vars_from_path(loader, path, entities, stage):

data = {}
if vars_loader._paths is None:
# cache has been reset, reload all()
_prime_vars_loader()
Comment on lines +83 to +85
Copy link
Contributor Author

@s-hertel s-hertel Jan 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used vars_loader._paths here as opposed to (for example) vars_loader._search_paths because I'm assuming we don't want to extend OLD_PLUGIN_CACHE_CLEARING to new caches.

I moved this into the vars plugin loader too. <- Actually can't, it duplicates any stateless enabled plugins.


if cached_vars_plugin_order is None:
_load_vars_plugins_order()

for plugin_name in cached_vars_plugin_order:
for plugin_name in vars_loader._plugin_instance_cache:
if (plugin := vars_loader.get(plugin_name)) is None:
continue

collection = '.' in plugin.ansible_name and not plugin.ansible_name.startswith('ansible.builtin.')
# Warn if a collection plugin has REQUIRES_ENABLED because it has no effect.
if collection and (hasattr(plugin, 'REQUIRES_ENABLED') or hasattr(plugin, 'REQUIRES_WHITELIST')):
display.warning(
"Vars plugins in collections must be enabled to be loaded, REQUIRES_ENABLED is not supported. "
"This should be removed from the plugin %s." % plugin.ansible_name
)

if not _plugin_should_run(plugin, stage):
continue

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- assert:
that:
- auto_role_var is defined
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from __future__ import annotations

from ansible.plugins.vars import BaseVarsPlugin


class VarsModule(BaseVarsPlugin):
# Implicitly
# REQUIRES_ENABLED = False

def get_vars(self, loader, path, entities):
return {'auto_role_var': True}
2 changes: 2 additions & 0 deletions test/integration/targets/old_style_vars_plugins/runme.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,5 @@ ANSIBLE_DEBUG=True ansible-playbook test_task_vars.yml > out.txt
[ "$(grep -c "Loading VarsModule 'host_group_vars'" out.txt)" -eq 1 ]
[ "$(grep -c "Loading VarsModule 'require_enabled'" out.txt)" -lt 3 ]
[ "$(grep -c "Loading VarsModule 'auto_enabled'" out.txt)" -gt 50 ]

ansible localhost -m include_role -a 'name=a' "$@"