Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
130 changes: 126 additions & 4 deletions salt/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
from salt.serializers.msgpack import deserialize as msgpack_deserialize
from salt.serializers.msgpack import serialize as msgpack_serialize
from salt.template import compile_template, compile_template_str
from salt.utils.datastructures import DefaultOrderedDict, HashableOrderedDict
from salt.utils.odict import DefaultOrderedDict, HashableOrderedDict
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't look right. Weren't all the usages intentionally moved out of odict to datastructures and odict deprecated?


log = logging.getLogger(__name__)

Expand Down Expand Up @@ -831,6 +831,8 @@ def __init__(
self.inject_globals = {}
self.mocked = mocked
self.global_state_conditions = None
# Fix for Issue #30971: Track processed SLS files to handle empty SLS files
self._processed_sls_files = set()

def _match_global_state_conditions(self, full, state, name):
"""
Expand Down Expand Up @@ -1231,8 +1233,6 @@ def _run_check_cmd(self, low_data):
cmd_opts = {}
if "shell" in self.opts["grains"]:
cmd_opts["shell"] = self.opts["grains"].get("shell")
if isinstance(low_data["check_cmd"], str):
low_data["check_cmd"] = [low_data["check_cmd"]]
Comment on lines -1234 to -1235
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks like it was inadvertently removed

for entry in low_data["check_cmd"]:
cmd = self.functions["cmd.retcode"](
entry, ignore_retcode=True, python_shell=True, **cmd_opts
Expand Down Expand Up @@ -1718,9 +1718,16 @@ def compile_high_data(self, high, orchestration_jid=None):
the individual state executor structures
"""
chunks = []
# Track all SLS files that were processed, even if they produced no chunks
# This is needed to handle SLS files that produce no output but are still
# required by other states (Issue #30971)
processed_sls_files = set()
Copy link
Contributor

Choose a reason for hiding this comment

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

This processed_sls_files variable appears to be unused. It is added to below but I don't see where it is ever read.

for name, body in high.items():
if name.startswith("__"):
continue
# Track SLS files from the high data, even if they produce no chunks
if "__sls__" in body:
processed_sls_files.add(body["__sls__"])
for state, run in body.items():
funcs = set()
names = []
Expand Down Expand Up @@ -3206,6 +3213,19 @@ def call_chunk(self, low, running, chunks, depth=0):
reqs.append(chunk)
found = True
continue
# If no chunks matched for SLS requisite, check if the SLS file
# was processed even if it produced no output (Issue #30971)
if req_key == "sls" and not found:
# Check if the SLS file was included/processed, even if it
# produced no chunks (empty SLS file)
processed_sls = getattr(self, "_processed_sls_files", set())
for processed_sls_file in processed_sls:
if fnmatch.fnmatch(processed_sls_file, req_val):
# SLS file was processed, even if empty
# Don't add to reqs (no chunks), but mark as found
# so it doesn't get added to lost
found = True
break
if fnmatch.fnmatch(chunk["name"], req_val) or fnmatch.fnmatch(
chunk["__id__"], req_val
):
Expand Down Expand Up @@ -3257,6 +3277,34 @@ def call_chunk(self, low, running, chunks, depth=0):
self.__run_num += 1
self.event(run_dict[tag], len(chunks), fire_event=low.get("fire_event"))
return running
# Fix for Issue #30971: If reqs is empty but we found empty SLS files
# in _processed_sls_files, we should skip the reqs loop and proceed
# to execute the chunk directly (empty SLS files have no chunks to process)
if not reqs:
# Check if any of the requisites were empty SLS files that were found
processed_sls = getattr(self, "_processed_sls_files", set())
has_empty_sls_requisite = False
for requisite in ["require", "require_any", "watch", "watch_any"]:
if requisite in low:
for req in low[requisite]:
if isinstance(req, dict):
req_key = next(iter(req))
if req_key == "sls":
req_val = req[req_key]
for processed_sls_file in processed_sls:
if fnmatch.fnmatch(processed_sls_file, req_val):
has_empty_sls_requisite = True
break
if has_empty_sls_requisite:
break
if has_empty_sls_requisite:
break

# If we have empty SLS requisites that were found, skip reqs processing
# and proceed to execute the chunk directly
if not has_empty_sls_requisite:
# No empty SLS requisites, this is a real error
pass # Will be handled by the code below
for chunk in reqs:
# Check to see if the chunk has been run, only run it if
# it has not been run already
Expand Down Expand Up @@ -3290,6 +3338,48 @@ def call_chunk(self, low, running, chunks, depth=0):
if self.check_failhard(chunk, running):
running["__FAILHARD__"] = True
return running
# Fix for Issue #30971: If reqs is empty because we found empty SLS files
# in _processed_sls_files, we should execute the chunk directly without recursion
if not reqs:
# Check if any requisites were empty SLS files
processed_sls = getattr(self, "_processed_sls_files", set())
has_empty_sls_requisite = False
for requisite in ["require", "require_any", "watch", "watch_any"]:
if requisite in low:
for req in low[requisite]:
if isinstance(req, dict):
req_key = next(iter(req))
if req_key == "sls":
req_val = req[req_key]
for processed_sls_file in processed_sls:
if fnmatch.fnmatch(processed_sls_file, req_val):
has_empty_sls_requisite = True
break
if has_empty_sls_requisite:
break
if has_empty_sls_requisite:
break

# If we have empty SLS requisites, execute the chunk directly
if has_empty_sls_requisite:
# Empty SLS files were required and found, execute chunk directly
# without recursion (no chunks to process from empty SLS files)
# We treat this as if requisites are "met" since empty SLS files
# have no chunks to satisfy, but the SLS file itself was found
if low.get("__prereq__"):
status, reqs = self.check_requisite(
low, running, chunks, pre=True
)
self.pre[tag] = self.call(low, chunks, running)
if not self.pre[tag]["changes"] and status == "change":
self.pre[tag]["changes"] = {"watch": "watch"}
self.pre[tag]["result"] = None
else:
# Execute the state directly - empty SLS requisites are satisfied
# so we can proceed to execute this chunk
running[tag] = self.call(low, chunks, running)
return running

if low.get("__prereq__"):
status, reqs = self.check_requisite(low, running, chunks)
self.pre[tag] = self.call(low, chunks, running)
Expand Down Expand Up @@ -3587,6 +3677,9 @@ def call_high(self, high, orchestration_jid=None):
Process a high data call and ensure the defined states.
"""
errors = []
# Initialize _processed_sls_files if not already set (Issue #30971)
if not hasattr(self, "_processed_sls_files"):
self._processed_sls_files = set()
# If there is extension data reconcile it
high, ext_errors = self.reconcile_extend(high)
errors.extend(ext_errors)
Expand Down Expand Up @@ -4393,7 +4486,15 @@ def render_state(self, sls, saltenv, mods, matches, local=False, context=None):
except AttributeError:
pass

if state:
# Fix for Issue #30971: Track SLS files that were rendered, even if they
# produce no output (empty state), so they can satisfy requisites
if hasattr(self.state, "_processed_sls_files"):
self.state._processed_sls_files.add(sls)

# Process state even if it's empty (Issue #30971)
# Empty states may have includes that need to be processed, and we need
# to track them for requisite checking
if state is not None:
if not isinstance(state, dict):
errors.append(f"SLS {sls} does not render to a dictionary")
else:
Expand Down Expand Up @@ -4435,6 +4536,11 @@ def render_state(self, sls, saltenv, mods, matches, local=False, context=None):
errors.append(msg)
continue

# Fix for Issue #30971: Track included SLS files even if they
# produce no output, so they can satisfy requisites
if hasattr(self.state, "_processed_sls_files"):
self.state._processed_sls_files.add(inc_sls)

if inc_sls.startswith("."):
match = re.match(r"^(\.+)(.*)$", inc_sls)
if match:
Expand Down Expand Up @@ -4500,6 +4606,10 @@ def render_state(self, sls, saltenv, mods, matches, local=False, context=None):
)
mod_tgt = f"{r_env}:{sls_target}"
if mod_tgt not in mods:
# Fix for Issue #30971: Track included SLS files even if they
# produce no output, so they can satisfy requisites
if hasattr(self.state, "_processed_sls_files"):
self.state._processed_sls_files.add(sls_target)
nstate, err = self.render_state(
sls_target,
r_env,
Expand Down Expand Up @@ -4692,6 +4802,10 @@ def render_highstate(self, matches, context=None):
all_errors = []
mods = set()
statefiles = []
# Track all SLS files that were rendered, even if they produced no output
# This is needed to handle SLS files that produce no output but are still
# required by other states (Issue #30971)
rendered_sls_files = set()
for saltenv, states in matches.items():
for sls_match in states:
if saltenv in self.avail:
Expand All @@ -4714,6 +4828,8 @@ def render_highstate(self, matches, context=None):
r_env = f"{saltenv}:{sls}"
if r_env in mods:
continue
# Track that this SLS file was rendered, even if it produces no output
rendered_sls_files.add(sls)
state, errors = self.render_state(
sls, saltenv, mods, matches, context=context
)
Expand All @@ -4732,6 +4848,12 @@ def render_highstate(self, matches, context=None):
all_errors.extend(errors)

self.clean_duplicate_extends(highstate)
# Store rendered SLS files for requisite checking (Issue #30971)
# This allows us to track SLS files that were rendered but produced no output
if hasattr(self, "state") and hasattr(self.state, "_processed_sls_files"):
self.state._processed_sls_files.update(rendered_sls_files)
elif hasattr(self, "state"):
self.state._processed_sls_files = rendered_sls_files
return highstate, all_errors

def clean_duplicate_extends(self, highstate):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -694,3 +694,62 @@ def test_issue_61121_extend_is_to_strict(state, state_tree):
ret = state.sls("requisite")
result = normalize_ret(ret.raw)
assert result == expected_result


def test_issue_30971_sls_empty_output_requisite_not_found(state, state_tree):
"""
Test that requiring an SLS file that produces no output (empty) should
not result in "requisites were not found" error.

Issue #30971: When an SLS file has conditional logic that results in no states
being generated (e.g., empty pillar data), a state that requires that SLS
file fails with "The following requisites were not found: require: sls:"

Expected behavior: The SLS file should be considered "satisfied" even if
it produces no output, and the requiring state should run successfully.
"""
# First SLS file that produces no output (empty due to conditional)
# This simulates the repos.custom file that produces no states when
# pillar data is empty
empty_sls_contents = """
{% if False %}
# This will never execute, so this SLS file produces no states
test_state:
test.succeed_without_changes:
- name: test
{% endif %}
"""

# Second SLS file that requires the first one
requiring_sls_contents = """
include:
- empty_sls

test_requiring_state:
test.succeed_without_changes:
- name: test_requiring
- require:
- sls: empty_sls
"""

with pytest.helpers.temp_file(
"empty_sls.sls", empty_sls_contents, state_tree
), pytest.helpers.temp_file("requiring.sls", requiring_sls_contents, state_tree):
ret = state.sls("requiring")

# The bug: This will fail with "The following requisites were not found"
# Expected: The state should succeed even though empty_sls produced no output
for state_return in ret:
# This assertion will fail with the bug - the state will have result=False
# and comment containing "The following requisites were not found"
assert state_return.result is True, (
f"State {state_return.name} failed: {state_return.comment}. "
"This is the bug: SLS files that produce no output should still "
"satisfy requisites."
)
assert (
"The following requisites were not found" not in state_return.comment
), (
f"State {state_return.name} incorrectly reports requisites not found. "
"This is the bug: requiring an empty SLS file should not fail."
)
Loading