Skip to content

Commit e07e131

Browse files
committed
Fix __pillar__ access in modules in pillar render
Historically the __pillar__ dunder has never been available or worked in extension modules during pillar render, due to what I can only assume is a bug/oversight in the overly-complex mess of branches and logic in the pillar rendering code. Functionally there is very little reason this shouldn't work, and it even appears like it may have worked at some point, or at least was intended to work by the fact that the ext_pillar data was stuffed right back into opts before attempting to render the remainder of the pillar. The missing piece to the puzzle here was that the partially-rendered ext_pillar data wasn't available in the opts dict packed into the salt.loader.minion_mods() call when it was generated, and the fact that the functions weren't reloaded after the pillar dict was updated. This commit fixes both of those things. Ensure the minion_mods are *always* reloaded with the updated opts dict containing the partial pillar data when rendering pillar with ext_pillar_first enabled. Signed-off-by: Joe Groocock <jgroocock@cloudflare.com>
1 parent c92ff12 commit e07e131

File tree

3 files changed

+54
-10
lines changed

3 files changed

+54
-10
lines changed

changelog/63398.fixed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix __pillar__ access in execution modules during pillar render with ext_pillar_first

salt/pillar/__init__.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,7 @@ def __init__(
537537
if opts.get("pillarenv_from_saltenv", False):
538538
opts["pillarenv"] = saltenv
539539
# use the local file client
540+
self.raw_opts = opts
540541
self.opts = self.__gen_opts(opts, grains, saltenv=saltenv, pillarenv=pillarenv)
541542
self.saltenv = saltenv
542543
self.client = salt.fileclient.get_file_client(self.opts, True)
@@ -547,16 +548,8 @@ def __init__(
547548
):
548549
opts["grains"] = grains
549550

550-
# if we didn't pass in functions, lets load them
551-
if functions is None:
552-
utils = salt.loader.utils(opts)
553-
if opts.get("file_client", "") == "local":
554-
self.functions = salt.loader.minion_mods(opts, utils=utils)
555-
else:
556-
self.functions = salt.loader.minion_mods(self.opts, utils=utils)
557-
else:
558-
self.functions = functions
559-
551+
self.utils = None
552+
self.functions = self.__gen_functions(functions)
560553
self.opts["minion_id"] = minion_id
561554
self.matchers = salt.loader.matchers(self.opts)
562555
self.rend = salt.loader.render(self.opts, self.functions)
@@ -620,6 +613,29 @@ def __gather_avail(self):
620613
avail[saltenv] = self.client.list_states(saltenv)
621614
return avail
622615

616+
def __gen_functions(self, context=None, functions=None, force=False):
617+
"""
618+
Generates the minion_mods loader object using the correct opts, context
619+
and pillar values in the correct scenarios
620+
"""
621+
if not force and functions is not None:
622+
return functions
623+
624+
# This is some historical cruft that I don't want to pretend to try to
625+
# understand. Keeping compatible for the sake of compatibility.
626+
# Origin: 340aed8049b885c0736681e925cf7210ede0f961
627+
opts = self.opts
628+
if self.raw_opts.get("file_client", "") == "local":
629+
opts = self.raw_opts
630+
# Ensure we keep the partially-rendered pillar regardless of which
631+
# opts we're using. Copy pillar into the alternative opts.
632+
if "pillar" in self.opts:
633+
opts["pillar"] = self.opts["pillar"]
634+
635+
if self.utils is None:
636+
self.utils = salt.loader.utils(self.raw_opts, context=context)
637+
return salt.loader.minion_mods(opts, utils=self.utils, context=context)
638+
623639
def __gen_opts(self, opts_in, grains, saltenv=None, ext=None, pillarenv=None):
624640
"""
625641
The options need to be altered to conform to the file client
@@ -1217,6 +1233,8 @@ def compile_pillar(self, ext=True):
12171233
if ext:
12181234
if self.opts.get("ext_pillar_first", False):
12191235
self.opts["pillar"], errors = self.ext_pillar(self.pillar_override)
1236+
# Reload minion_mods with ext_pillar as pillar dict
1237+
self.functions = self.__gen_functions(force=True)
12201238
self.rend = salt.loader.render(self.opts, self.functions)
12211239
matches = self.top_matches(top, reload=True)
12221240
pillar, errors = self.render_pillar(matches, errors=errors)

tests/pytests/unit/pillar/test_pillar.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import salt.loader
44
import salt.pillar
55
from salt.utils.odict import OrderedDict
6+
from tests.support.mock import patch
67

78

89
@pytest.mark.parametrize(
@@ -123,3 +124,27 @@ def test_pillar_envs_path_substitution(env, temp_salt_minion, tmp_path):
123124

124125
# The __env__ string in the path has been substituted for the actual env
125126
assert pillar.opts["pillar_roots"] == expected
127+
128+
129+
def test_ext_pillar_dunder_in_modules_in_pillar(temp_salt_minion):
130+
"""
131+
Test that ext_pillar is available in __pillar__ inside execution modules
132+
during pillar render when using ext_pillar_first=true
133+
"""
134+
opts = temp_salt_minion.config.copy()
135+
opts["ext_pillar_first"] = True
136+
137+
grains = salt.loader.grains(opts)
138+
pillar = salt.pillar.Pillar(opts, grains, temp_salt_minion.id, "base")
139+
# Pillar should be empty to start with
140+
assert pillar.functions.pack["__pillar__"] == {}
141+
142+
ext_value = {"ext": "some ext value"}
143+
pil_value = {"pillar": "some pillar value"}
144+
with patch.object(pillar, "ext_pillar", return_value=(ext_value, [])):
145+
with patch.object(pillar, "render_pillar", return_value=(pil_value, [])):
146+
compiled = pillar.compile_pillar()
147+
assert compiled == dict(**ext_value, **pil_value)
148+
149+
# Loader should pack the opts pillar dict from the ext_pillar() call
150+
assert pillar.functions.pack["__pillar__"] == ext_value

0 commit comments

Comments
 (0)