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
8 changes: 8 additions & 0 deletions salt/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,8 @@ def compile_high_data(
the individual state executor structures
"""
self.dependency_dag = DependencyGraph()
if getattr(self, "_rendered_sls", None):
self.dependency_dag.set_rendered_sls(self._rendered_sls)
chunks = []
for name, body in high.items():
if name.startswith("__"):
Expand Down Expand Up @@ -1668,6 +1670,8 @@ def compile_high_data(
return a tuple of the LowChunk structures and a list of errors
"""
self.dependency_dag = DependencyGraph()
if getattr(self, "_rendered_sls", None):
self.dependency_dag.set_rendered_sls(self._rendered_sls)
chunks = []
disabled = {}
agg_opt = self.functions["config.option"]("state_aggregate")
Expand Down Expand Up @@ -4594,6 +4598,10 @@ def render_highstate(self, matches, context=None):
all_errors.extend(errors)

self.clean_duplicate_extends(highstate)
# Track SLS files successfully rendered (even if they produced zero
# states) so the dependency graph can distinguish "rendered but empty"
# SLS files from genuinely missing ones. Fixes #30971.
self._rendered_sls = mods
return highstate, all_errors

def clean_duplicate_extends(self, highstate):
Expand Down
33 changes: 32 additions & 1 deletion salt/utils/requisite.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,14 +340,25 @@ class DependencyGraph:
between the states.
"""

__slots__ = ("dag", "nodes_lookup_map", "sls_to_nodes")
__slots__ = ("dag", "nodes_lookup_map", "sls_to_nodes", "rendered_sls")

def __init__(self) -> None:
self.dag = MultiDiGraph()
# a mapping to node_id to be able to find nodes with
# specific state type (module name), names, and/or IDs
self.nodes_lookup_map: dict[tuple[str, str], set[str]] = {}
self.sls_to_nodes: dict[str, set[str]] = {}
# SLS files rendered but may have produced zero states (issue #30971)
self.rendered_sls: set[str] | None = None

def set_rendered_sls(self, rendered_sls: set[str]) -> None:
"""Register SLS files that were successfully rendered.

These SLS files were found and processed but may have produced
zero state IDs. When a ``require sls: ...`` references one of
these, the requisite is satisfied rather than failing.
"""
self.rendered_sls = rendered_sls

def _add_prereq(self, node_tag: str, req_tag: str):
# the prerequiring chunk is the state declaring the prereq
Expand Down Expand Up @@ -469,6 +480,21 @@ def _get_prereq_node_tag(self, low_tag: str):
def _is_fnmatch_pattern(self, value: str) -> bool:
return any(char in value for char in ("*", "?", "[", "]"))

def _sls_was_rendered(self, sls_path: str) -> bool:
"""Check if *sls_path* was rendered even if it produced no states.

``rendered_sls`` stores entries with the ``saltenv:sls`` format.
Strip the saltenv prefix when comparing so that a require on just
the SLS path matches.
"""
if not self.rendered_sls:
return False
for entry in self.rendered_sls:
_, colon, sls = entry.partition(":")
if colon and sls == sls_path:
return True
return False

def _chunk_str(self, chunk: LowChunk) -> str:
node_dict = {
"SLS": chunk["__sls__"],
Expand Down Expand Up @@ -516,6 +542,11 @@ def add_dependency(
if req_tags := self.sls_to_nodes.get(req_val, []):
found = True
self._add_reqs(node_tag, has_prereq_node, req_type, req_tags)
elif self.rendered_sls and self._sls_was_rendered(req_val):
# The SLS file was rendered but produced no states
# (e.g. Jinja loop over empty pillar). Treat the
# require as satisfied — issue #30971.
found = True
elif self._is_fnmatch_pattern(req_val):
# This iterates over every chunk to check
# if any match instead of doing a look up since
Expand Down
Loading