Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
23b586f
Access pillar data while rendering pillars
dwoz Apr 8, 2023
aca0923
Fix unit test
dwoz Nov 8, 2023
c2e8a63
Add test from alt PR
dwoz Nov 8, 2023
e59134c
Fix matchers using pillar in opts
dwoz Dec 18, 2023
47bdd5d
Add changelog for #65724
dwoz Dec 18, 2023
e879d56
Fix linter
dwoz Dec 18, 2023
b147108
Fix typo
dwoz Apr 8, 2024
30e5e7d
Fix test assertion for ext_pillar_first
dwoz Apr 4, 2026
6db0c16
Address frebib's PR comments: ensure opts['pillar'] is set in __init_…
dwoz Apr 5, 2026
dd95be0
Fix linter: remove trailing newlines
dwoz Apr 5, 2026
1393192
Fully address frebib's suggestions: update loader pack instead of rel…
dwoz Apr 5, 2026
9a5f53c
Fix circular reference in Pillar when pillar_opts is True
dwoz Apr 5, 2026
80324e4
Fix integration regression: preserve master ID in ext_pillar_opts
dwoz Apr 6, 2026
63e7546
Final cleanup: surgical pillar data access, fixed regressions, and br…
dwoz Apr 9, 2026
cc6f9d4
Final cleanup: remove dangerous updates to pillar_data and ensure int…
dwoz Apr 9, 2026
c688319
Final cleanup: pop __context__ from master opts to prevent circular r…
dwoz Apr 10, 2026
067a76d
Final verified cleanup: resolved circular references and integration …
dwoz Apr 12, 2026
2827b27
Final verified fix: restore opts['pillar'] assignment for matchers in…
dwoz Apr 12, 2026
8f0b77a
Final verified cleanup: removed circular references and fixed integra…
dwoz Apr 12, 2026
45042a5
Final verified fix for Pillar regressions: resolved circular referenc…
dwoz Apr 13, 2026
251e6c8
Final verified cleanup: resolved circular references, integration reg…
dwoz Apr 13, 2026
7ee472e
Add changelog for #64043
dwoz Apr 13, 2026
3d26df2
Add explicit tests for pillar_opts, ssh_merge_pillar, decrypt_pillar …
dwoz Apr 14, 2026
79af111
Fix formatting and final logic verification for dunder pillar
dwoz Apr 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/64043.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added the ability to access already compiled pillar data during the pillar rendering process via the `__pillar__` global in templates and matchers.
1 change: 1 addition & 0 deletions changelog/65724.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Pillar dunder is now available in extension modules during pillar render.
50 changes: 40 additions & 10 deletions salt/loader/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ def minion_mods(
notify=False,
static_modules=None,
proxy=None,
pillar=None,
file_client=None,
):
"""
Expand Down Expand Up @@ -306,17 +307,20 @@ def minion_mods(
# TODO Publish documentation for module whitelisting
if not whitelist:
whitelist = opts.get("whitelist_modules", None)
pack = {
"__context__": context,
"__utils__": utils,
"__proxy__": proxy,
"__opts__": opts,
"__file_client__": file_client,
}
if pillar is not None:
pack["__pillar__"] = pillar
ret = LazyLoader(
_module_dirs(opts, "modules", "module"),
opts,
tag="module",
pack={
"__context__": context,
"__utils__": utils,
"__proxy__": proxy,
"__opts__": opts,
"__file_client__": file_client,
},
pack=pack,
whitelist=whitelist,
loaded_base_name=loaded_base_name,
static_modules=static_modules,
Expand Down Expand Up @@ -413,19 +417,35 @@ def metaproxy(opts, loaded_base_name=None):
)


def matchers(opts, loaded_base_name=None):
def matchers(opts, loaded_base_name=None, context=None, pillar=None):
"""
Return the matcher services plugins

:param dict opts: The Salt options dictionary
:param str loaded_base_name: The imported modules namespace when imported
by the salt loader.
:param dict context: The Salt context dictionary
:param dict pillar: The Salt pillar dictionary
"""
if context is None:
context = {}

pack = {
"__salt__": {},
"__runners__": {},
"__grains__": opts.get("grains", {}),
"__context__": context,
"__file_client__": None,
}
if pillar is not None:
pack["__pillar__"] = pillar

return LazyLoader(
_module_dirs(opts, "matchers"),
opts,
tag="matchers",
loaded_base_name=loaded_base_name,
pack=pack,
)


Expand Down Expand Up @@ -526,6 +546,7 @@ def utils(
context=None,
proxy=None,
file_client=None,
pillar=None,
pack_self=None,
loaded_base_name=None,
):
Expand All @@ -540,6 +561,9 @@ def utils(
:param str loaded_base_name: The imported modules namespace when imported
by the salt loader.
"""
pack = {"__context__": context, "__proxy__": proxy or {}}
if pillar is not None:
pack["__pillar__"] = pillar
return LazyLoader(
_module_dirs(opts, "utils", ext_type_dirs="utils_dirs", load_extensions=False),
opts,
Expand All @@ -556,7 +580,7 @@ def utils(
)


def pillars(opts, functions, context=None, loaded_base_name=None):
def pillars(opts, functions, context=None, pillar=None, loaded_base_name=None):
"""
Returns the pillars modules

Expand All @@ -568,11 +592,14 @@ def pillars(opts, functions, context=None, loaded_base_name=None):
by the salt loader.
"""
_utils = utils(opts)
pack = {"__salt__": functions, "__context__": context, "__utils__": _utils}
if pillar is not None:
pack["__pillar__"] = pillar
ret = LazyLoader(
_module_dirs(opts, "pillar"),
opts,
tag="pillar",
pack={"__salt__": functions, "__context__": context, "__utils__": _utils},
pack=pack,
extra_module_dirs=_utils.module_dirs,
pack_self="__ext_pillar__",
loaded_base_name=loaded_base_name,
Expand Down Expand Up @@ -916,6 +943,7 @@ def render(
proxy=None,
context=None,
file_client=None,
pillar=None,
loaded_base_name=None,
):
"""
Expand All @@ -939,6 +967,8 @@ def render(
"__context__": context,
"__file_client__": file_client,
}
if pillar is not None:
pack["__pillar__"] = pillar

if states:
pack["__states__"] = states
Expand Down
6 changes: 5 additions & 1 deletion salt/matchers/confirm_top.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ def confirm_top(match, data, nodegroups=None):
if "matchers" in __context__:
matchers = __context__["matchers"]
else:
matchers = salt.loader.matchers(__opts__)
# Matchers need pillar data if available
pillar = __pillar__ if "__pillar__" in globals() else None
if hasattr(pillar, "value"):
pillar = pillar.value()
matchers = salt.loader.matchers(__opts__, context=__context__, pillar=pillar)
__context__["matchers"] = matchers
funcname = matcher + "_match.match"
if matcher == "nodegroup":
Expand Down
11 changes: 8 additions & 3 deletions salt/matchers/pillar_exact_match.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,16 @@ def match(tgt, delimiter=":", opts=None, minion_id=None):
log.error("Got insufficient arguments for pillar match statement from master")
return False

if "pillar" in opts:
if opts.get("pillar"):
pillar = opts["pillar"]
elif "ext_pillar" in opts:
log.info("No pillar found, fallback to ext_pillar")
elif "__pillar__" in globals():
pillar = __pillar__
if hasattr(pillar, "value"):
pillar = pillar.value()
elif opts.get("ext_pillar"):
pillar = opts["ext_pillar"]
else:
pillar = {}

return salt.utils.data.subdict_match(
pillar, tgt, delimiter=delimiter, exact_match=True
Expand Down
11 changes: 8 additions & 3 deletions salt/matchers/pillar_match.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,15 @@ def match(tgt, delimiter=DEFAULT_TARGET_DELIM, opts=None, minion_id=None):
log.error("Got insufficient arguments for pillar match statement from master")
return False

if "pillar" in opts:
if opts.get("pillar"):
pillar = opts["pillar"]
elif "ext_pillar" in opts:
log.info("No pillar found, fallback to ext_pillar")
elif "__pillar__" in globals():
pillar = __pillar__
if hasattr(pillar, "value"):
pillar = pillar.value()
elif opts.get("ext_pillar"):
pillar = opts["ext_pillar"]
else:
pillar = {}

return salt.utils.data.subdict_match(pillar, tgt, delimiter=delimiter)
11 changes: 8 additions & 3 deletions salt/matchers/pillar_pcre_match.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,16 @@ def match(tgt, delimiter=DEFAULT_TARGET_DELIM, opts=None, minion_id=None):
)
return False

if "pillar" in opts:
if opts.get("pillar"):
pillar = opts["pillar"]
elif "ext_pillar" in opts:
log.info("No pillar found, fallback to ext_pillar")
elif "__pillar__" in globals():
pillar = __pillar__
if hasattr(pillar, "value"):
pillar = pillar.value()
elif opts.get("ext_pillar"):
pillar = opts["ext_pillar"]
else:
pillar = {}

return salt.utils.data.subdict_match(
pillar, tgt, delimiter=delimiter, regex_match=True
Expand Down
114 changes: 103 additions & 11 deletions salt/pillar/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,12 @@ def __init__(
self.client = salt.fileclient.get_file_client(self.opts, True)
self.fileclient = salt.fileclient.get_file_client(self.opts, False)
self.avail = self.__gather_avail()
self.pillar_data = self.opts.get("pillar", {})
if not isinstance(self.pillar_data, dict):
self.pillar_data = {}
else:
# Ensure we have a plain dict and not a proxy into opts
self.pillar_data = dict(self.pillar_data)

if opts.get("file_client", "") == "local" and not opts.get(
"use_master_when_local", False
Expand All @@ -432,20 +438,30 @@ def __init__(
opts,
utils=utils,
file_client=salt.fileclient.ContextlessFileClient(self.fileclient),
pillar=self.pillar_data,
)
else:
self.functions = salt.loader.minion_mods(
self.opts,
utils=utils,
file_client=salt.fileclient.ContextlessFileClient(self.fileclient),
pillar=self.pillar_data,
)
else:
self.functions = functions
if hasattr(self.functions, "pack"):
self.functions.pack["__pillar__"] = self.pillar_data

self.opts["minion_id"] = minion_id
self.matchers = salt.loader.matchers(self.opts)
self.matchers = salt.loader.matchers(self.opts, pillar=self.pillar_data)
if hasattr(self.matchers, "pack"):
self.matchers.pack["__pillar__"] = self.pillar_data
self.rend = salt.loader.render(
self.opts, self.functions, self.client, file_client=self.client
self.opts,
self.functions,
self.client,
file_client=self.client,
pillar=self.pillar_data,
)
ext_pillar_opts = copy.deepcopy(self.opts)
# Keep the incoming opts ID intact, ie, the master id
Expand All @@ -455,7 +471,9 @@ def __init__(
if opts.get("pillar_source_merging_strategy"):
self.merge_strategy = opts["pillar_source_merging_strategy"]

self.ext_pillars = salt.loader.pillars(ext_pillar_opts, self.functions)
self.ext_pillars = salt.loader.pillars(
ext_pillar_opts, self.functions, pillar=self.pillar_data
)
self.ignored_pillars = {}
self.pillar_override = pillar_override or {}
if not isinstance(self.pillar_override, dict):
Expand Down Expand Up @@ -737,7 +755,8 @@ def top_matches(self, top, reload=False):
"""
matches = {}
if reload:
self.matchers = salt.loader.matchers(self.opts)
self.matchers = salt.loader.matchers(self.opts, pillar=self.pillar_data)
self._update_loader_packs()
for saltenv, body in top.items():
if self.opts["pillarenv"]:
if saltenv != self.opts["pillarenv"]:
Expand Down Expand Up @@ -1110,29 +1129,49 @@ def compile_pillar(self, ext=True):
top, top_errors = self.get_top()
if ext:
if self.opts.get("ext_pillar_first", False):
self.opts["pillar"], errors = self.ext_pillar(self.pillar_override)
self.rend = salt.loader.render(self.opts, self.functions)
pillar, errors = self.ext_pillar(self.pillar_override)
Comment thread
dwoz marked this conversation as resolved.
self.pillar_data.update(pillar)
self._update_loader_packs()
self.rend = salt.loader.render(
self.opts,
self.functions,
self.client,
file_client=self.client,
pillar=self.pillar_data,
)
matches = self.top_matches(top, reload=True)
pillar, errors = self.render_pillar(matches, errors=errors)
pillar = merge(
self.opts["pillar"],
self.pillar_data,
pillar,
self.merge_strategy,
self.opts.get("renderer", "yaml"),
self.opts.get("pillar_merge_lists", False),
)
self.pillar_data.update(pillar)
self._update_loader_packs()
else:
matches = self.top_matches(top)
pillar, errors = self.render_pillar(matches)
pillar, errors = self.ext_pillar(pillar, errors=errors)
self.pillar_data.update(pillar)
self._update_loader_packs()
pillar, errors = self.ext_pillar(self.pillar_data, errors=errors)
self.pillar_data.update(pillar)
self._update_loader_packs()
else:
matches = self.top_matches(top)
pillar, errors = self.render_pillar(matches)
self.pillar_data.update(pillar)
self._update_loader_packs()

pillar = self.pillar_data
errors.extend(top_errors)
if self.opts.get("pillar_opts", False):
mopts = dict(self.opts)
if "grains" in mopts:
mopts.pop("grains")
mopts = {}
for key, val in self.opts.items():
if key in ("pillar", "__context__", "functions", "matchers", "rend"):
continue
mopts[key] = val
mopts["saltversion"] = __version__
pillar["master"] = mopts
if "pillar" in self.opts and self.opts.get("ssh_merge_pillar", False):
Expand Down Expand Up @@ -1160,8 +1199,61 @@ def compile_pillar(self, ext=True):
decrypt_errors = self.decrypt_pillar(pillar)
if decrypt_errors:
pillar.setdefault("_errors", []).extend(decrypt_errors)
self.pillar_data.update(pillar)
self._update_loader_packs()
return pillar

def _update_loader_packs(self):
"""
Update the loader packs with the current pillar data
"""
for loader in (self.functions, self.matchers):
if hasattr(loader, "pack"):
if "__pillar__" in loader.pack:
if loader.pack["__pillar__"] is self.pillar_data:
continue
if isinstance(loader.pack["__pillar__"], dict):
loader.pack["__pillar__"].clear()
loader.pack["__pillar__"].update(self.pillar_data)
else:
loader.pack["__pillar__"] = self.pillar_data
else:
loader.pack["__pillar__"] = self.pillar_data

# Also update matchers in context if they exist
context = {}
if hasattr(self.functions, "pack"):
context = self.functions.pack.get("__context__", {})
if not context and hasattr(self.matchers, "pack"):
context = self.matchers.pack.get("__context__", {})

if hasattr(context, "value"):
context = context.value()

if isinstance(context, dict) and "matchers" in context:
m = context["matchers"]
if hasattr(m, "pack"):
if "__pillar__" in m.pack:
if m.pack["__pillar__"] is not self.pillar_data:
if isinstance(m.pack["__pillar__"], dict):
m.pack["__pillar__"].clear()
m.pack["__pillar__"].update(self.pillar_data)
else:
m.pack["__pillar__"] = self.pillar_data
else:
m.pack["__pillar__"] = self.pillar_data

if hasattr(self.rend, "pack"):
if "__pillar__" in self.rend.pack:
if self.rend.pack["__pillar__"] is not self.pillar_data:
if isinstance(self.rend.pack["__pillar__"], dict):
self.rend.pack["__pillar__"].clear()
self.rend.pack["__pillar__"].update(self.pillar_data)
else:
self.rend.pack["__pillar__"] = self.pillar_data
else:
self.rend.pack["__pillar__"] = self.pillar_data

def decrypt_pillar(self, pillar):
"""
Decrypt the specified pillar dictionary items, if configured to do so
Expand Down
Loading
Loading