From 0dc478e781a2b0c0ffa1cdefb35a42dee9bb894c Mon Sep 17 00:00:00 2001 From: Joaquin Hui Gomez <132194176+joaquinhuigomez@users.noreply.github.com> Date: Sat, 28 Mar 2026 23:27:47 +0000 Subject: [PATCH 1/3] fix: guard against non-iterable target in list_jobs search_target When search_target is used in list_jobs, the code assumed Target would always be a string or list. Execution/state failures can leave Target as a non-iterable value (e.g. int), causing a TypeError. Skip non-iterable targets instead of crashing. Closes #68780 --- salt/runners/jobs.py | 2 ++ tests/pytests/unit/runners/test_jobs.py | 42 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/salt/runners/jobs.py b/salt/runners/jobs.py index f1ff0f8b4921..2f530d6b20c7 100644 --- a/salt/runners/jobs.py +++ b/salt/runners/jobs.py @@ -334,6 +334,8 @@ def list_jobs( targets = ret[item]["Target"] if isinstance(targets, str): targets = [targets] + elif not isinstance(targets, (list, tuple)): + targets = [] for target in targets: for key in salt.utils.args.split_input(search_target): if fnmatch.fnmatch(target, key): diff --git a/tests/pytests/unit/runners/test_jobs.py b/tests/pytests/unit/runners/test_jobs.py index 8d9fe3853a77..0edb67304743 100644 --- a/tests/pytests/unit/runners/test_jobs.py +++ b/tests/pytests/unit/runners/test_jobs.py @@ -70,3 +70,45 @@ def __init__(self, *args, **kwargs): assert jobs.list_jobs(search_target="node-1-2.com") == returns["node-1-2.com"] assert jobs.list_jobs(search_target="non-existant") == returns["non-existant"] + + +def test_list_jobs_with_non_iterable_target(): + """ + test jobs.list_jobs does not crash when Target is not iterable (e.g. int) + + Regression test for https://github.com/saltstack/salt/issues/68780 + """ + mock_jobs_cache = { + "20160524035503086853": { + "Arguments": [], + "Function": "test.ping", + "StartTime": "2016, May 24 03:55:03.086853", + "Target": 3, + "Target-type": "glob", + "User": "root", + }, + "20160524035524895387": { + "Arguments": [], + "Function": "test.ping", + "StartTime": "2016, May 24 03:55:24.895387", + "Target": "node-1-1.com", + "Target-type": "glob", + "User": "sudo_ubuntu", + }, + } + + def return_mock_jobs(): + return mock_jobs_cache + + class MockMasterMinion: + + returners = {"local_cache.get_jids": return_mock_jobs} + + def __init__(self, *args, **kwargs): + pass + + with patch.object(salt.minion, "MasterMinion", MockMasterMinion): + # Should not raise TypeError; the non-iterable target job is skipped + result = jobs.list_jobs(search_target="node-1-1.com") + assert "20160524035524895387" in result + assert "20160524035503086853" not in result From 2eec40ca89cc2cffb526b70ac4c30b59309225a8 Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Sun, 7 Jun 2026 16:43:43 -0700 Subject: [PATCH 2/3] Log warning on non-iterable Target skip + add changelog for #68780 --- changelog/68780.fixed.md | 1 + salt/runners/jobs.py | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 changelog/68780.fixed.md diff --git a/changelog/68780.fixed.md b/changelog/68780.fixed.md new file mode 100644 index 000000000000..379a45b3f943 --- /dev/null +++ b/changelog/68780.fixed.md @@ -0,0 +1 @@ +Guard against non-iterable `Target` in cached job entries when filtering by `search_target` in `salt-run jobs.list_jobs`. diff --git a/salt/runners/jobs.py b/salt/runners/jobs.py index 2f530d6b20c7..f163eb55a43d 100644 --- a/salt/runners/jobs.py +++ b/salt/runners/jobs.py @@ -335,6 +335,9 @@ def list_jobs( if isinstance(targets, str): targets = [targets] elif not isinstance(targets, (list, tuple)): + log.warning( + "Job %s has non-iterable Target %r; skipping", item, targets + ) targets = [] for target in targets: for key in salt.utils.args.split_input(search_target): From 1b4be8ed42481a0c6fd69db677f8e168a1c33c24 Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Fri, 26 Jun 2026 06:03:29 -0700 Subject: [PATCH 3/3] Handle any non-str iterable Target in list_jobs search_target Address review concern: accept any iterable Target (set, dict, etc.) not just list/tuple; only skip and warn on non-iterable scalars like int. Add test covering set-typed Target. Fixes #68780 --- salt/runners/jobs.py | 8 +++-- tests/pytests/unit/runners/test_jobs.py | 42 +++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/salt/runners/jobs.py b/salt/runners/jobs.py index f163eb55a43d..8a27f42e193b 100644 --- a/salt/runners/jobs.py +++ b/salt/runners/jobs.py @@ -334,9 +334,13 @@ def list_jobs( targets = ret[item]["Target"] if isinstance(targets, str): targets = [targets] - elif not isinstance(targets, (list, tuple)): + elif hasattr(targets, "__iter__"): + targets = list(targets) + else: log.warning( - "Job %s has non-iterable Target %r; skipping", item, targets + "Job %s has a non-iterable Target value %r; skipping", + item, + targets, ) targets = [] for target in targets: diff --git a/tests/pytests/unit/runners/test_jobs.py b/tests/pytests/unit/runners/test_jobs.py index 0edb67304743..2fab17642814 100644 --- a/tests/pytests/unit/runners/test_jobs.py +++ b/tests/pytests/unit/runners/test_jobs.py @@ -112,3 +112,45 @@ def __init__(self, *args, **kwargs): result = jobs.list_jobs(search_target="node-1-1.com") assert "20160524035524895387" in result assert "20160524035503086853" not in result + + +def test_list_jobs_with_set_target(): + """ + test jobs.list_jobs handles Target stored as a set (any non-str iterable) + + Regression test for https://github.com/saltstack/salt/issues/68780 + """ + mock_jobs_cache = { + "20160524035503086853": { + "Arguments": [], + "Function": "test.ping", + "StartTime": "2016, May 24 03:55:03.086853", + "Target": {"node-1-1.com", "node-1-2.com"}, + "Target-type": "list", + "User": "root", + }, + "20160524035524895387": { + "Arguments": [], + "Function": "test.ping", + "StartTime": "2016, May 24 03:55:24.895387", + "Target": "node-2-1.com", + "Target-type": "glob", + "User": "sudo_ubuntu", + }, + } + + def return_mock_jobs(): + return mock_jobs_cache + + class MockMasterMinion: + + returners = {"local_cache.get_jids": return_mock_jobs} + + def __init__(self, *args, **kwargs): + pass + + with patch.object(salt.minion, "MasterMinion", MockMasterMinion): + # Should not raise TypeError; set target should be iterated correctly + result = jobs.list_jobs(search_target="node-1-1.com") + assert "20160524035503086853" in result + assert "20160524035524895387" not in result