-
Notifications
You must be signed in to change notification settings - Fork 5.6k
Fix #30971: Handle requisites correctly for empty SLS files #68534
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
base: 3006.x
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
||
| log = logging.getLogger(__name__) | ||
|
|
||
|
|
@@ -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): | ||
| """ | ||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
@@ -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() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This |
||
| 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 = [] | ||
|
|
@@ -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 | ||
| ): | ||
|
|
@@ -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 | ||
|
|
@@ -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) | ||
|
|
@@ -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) | ||
|
|
@@ -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: | ||
|
|
@@ -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: | ||
|
|
@@ -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, | ||
|
|
@@ -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: | ||
|
|
@@ -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 | ||
| ) | ||
|
|
@@ -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): | ||
|
|
||
There was a problem hiding this comment.
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?