diff --git a/changelog/57999.fixed.md b/changelog/57999.fixed.md new file mode 100644 index 00000000000..456462f51cf --- /dev/null +++ b/changelog/57999.fixed.md @@ -0,0 +1 @@ +Fixed an ``AttributeError`` (``'OrderedDict' object has no attribute 'startswith'``) from the state compiler when a ``*_in`` requisite triggered the name-resolution fallback scan while an ``exclude`` (by id or sls) was in effect. The scan now skips non-state entries such as ``__exclude__``. diff --git a/salt/state.py b/salt/state.py index 5a2829d2c05..e4a1feafe9b 100644 --- a/salt/state.py +++ b/salt/state.py @@ -2058,6 +2058,12 @@ def requisite_in(self, high): else: found = False for _id in iter(high): + # Skip non-state entries such as + # __exclude__ (a list), whose values + # are not state bodies -- mirrors the + # guard in the top-level loop above. + if not isinstance(high[_id], dict): + continue for st8 in [ _st8 for _st8 in iter(high[_id]) diff --git a/tests/pytests/unit/state/test_state_compiler.py b/tests/pytests/unit/state/test_state_compiler.py index 4a546ce1dfe..479c888f7c5 100644 --- a/tests/pytests/unit/state/test_state_compiler.py +++ b/tests/pytests/unit/state/test_state_compiler.py @@ -1,5 +1,5 @@ """ - :codeauthor: Nicole Thomas +:codeauthor: Nicole Thomas """ import logging @@ -96,6 +96,53 @@ def test_render_error_on_invalid_requisite(minion_opts): state_obj.call_high(high_data) +def test_requisite_in_with_exclude_does_not_raise(minion_opts): + """ + requisite_in must not crash when ``__exclude__`` is present and a ``*_in`` + requisite forces the name-resolution fallback scan, which iterates every + top-level entry in the high data -- including ``__exclude__`` (a list). + Regression test for #57999 (AttributeError: 'OrderedDict' object has no + attribute 'startswith'). + """ + with patch("salt.state.State._gather_pillar"): + high_data = { + "create_file": salt.state.HashableOrderedDict( + [ + ("file", ["managed", {"name": "/tmp/57999"}]), + ("__sls__", "issue_57999"), + ("__env__", "base"), + ] + ), + "notify_on_change": salt.state.HashableOrderedDict( + [ + ( + "cmd", + [ + "run", + {"name": "true"}, + # Bare-string target that is not an id, so + # requisite_in falls back to scanning every entry in + # high (by name) -- the scan that used to choke on + # the __exclude__ list. + {"onchanges_in": ["not-an-existing-id"]}, + ], + ), + ("__sls__", "issue_57999"), + ("__env__", "base"), + ] + ), + "__exclude__": [{"id": "create_file"}], + } + state_obj = salt.state.State(minion_opts) + # Prior to the fix this raised AttributeError on the __exclude__ entry. + high_ret, errors = state_obj.requisite_in(high_data) + assert errors == [] + # __exclude__ is left for apply_exclude to consume later; the real + # states are untouched by requisite_in. + assert "create_file" in high_ret + assert "notify_on_change" in high_ret + + def test_verify_onlyif_parse(minion_opts): low_data = { "onlyif": [{"fun": "test.arg", "args": ["arg1", "arg2"]}],