From 5625a23445c6f097524f1fb359952b0c6984ff15 Mon Sep 17 00:00:00 2001 From: "Gary T. Giesen" Date: Sat, 4 Jul 2026 16:18:17 -0400 Subject: [PATCH 1/2] Honour allow_updates for pkg.installed sources installs _find_install_targets discarded the allow_updates flag whenever a package came from sources: (allow_updates = bool(not sources and ...)), forcing an exact-version match against the version reported by the source package. A self-updating agent installed via sources would therefore be reinstalled or downgraded on every highstate. Drop the 'not sources' guard so allow_updates is honoured for sources installs, and document that behaviour. Fixes #35385 --- changelog/35385.fixed.md | 1 + salt/states/pkg.py | 6 ++-- tests/pytests/unit/states/test_pkg.py | 42 +++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 changelog/35385.fixed.md diff --git a/changelog/35385.fixed.md b/changelog/35385.fixed.md new file mode 100644 index 000000000000..4e3d9feb3552 --- /dev/null +++ b/changelog/35385.fixed.md @@ -0,0 +1 @@ +Fixed pkg.installed to honour allow_updates for packages installed via sources, so a newer installed version is no longer reinstalled or downgraded on every run. diff --git a/salt/states/pkg.py b/salt/states/pkg.py index cd586623ff1f..e6bcd2364fc7 100644 --- a/salt/states/pkg.py +++ b/salt/states/pkg.py @@ -811,7 +811,7 @@ def _find_install_targets( altered_files[package_name] = verify_result continue version_fulfilled = False - allow_updates = bool(not sources and kwargs.get("allow_updates")) + allow_updates = bool(kwargs.get("allow_updates")) try: version_fulfilled = _fulfills_version_string( cver, verstr, ignore_epoch=ignore_epoch, allow_updates=allow_updates @@ -1274,7 +1274,9 @@ def installed( Allow the package to be updated outside Salt's control (e.g. auto updates on Windows). This means a package on the Minion can have a newer version than the latest available in the repository without - enforcing a re-installation of the package. + enforcing a re-installation of the package. This also applies to + packages installed via ``sources``, where a newer installed version + will not be downgraded to the version reported by the source package. .. versionadded:: 2014.7.0 diff --git a/tests/pytests/unit/states/test_pkg.py b/tests/pytests/unit/states/test_pkg.py index e9e6f3060d16..099fd52fd2e2 100644 --- a/tests/pytests/unit/states/test_pkg.py +++ b/tests/pytests/unit/states/test_pkg.py @@ -602,6 +602,48 @@ def test_installed_with_sources(list_pkgs, tmp_path): raise exc from None +def test_installed_sources_allow_updates_no_downgrade(): + """ + pkg.installed with ``sources`` and ``allow_updates=True`` should not + reinstall/downgrade when the installed version is newer than the version + reported by the source package. Regression test for #35385. + """ + source = "salt://server/check-mk-agent.deb" + + def _list_pkgs(**kwargs): + if kwargs.get("purge_desired"): + return {} + return {"check-mk-agent": ["2.0.0"]} + + with patch.dict( + pkg.__salt__, + { + "pkg.list_pkgs": MagicMock(side_effect=_list_pkgs), + "pkg_resource.pack_sources": MagicMock( + return_value={"check-mk-agent": source} + ), + "cp.cache_file": MagicMock(return_value="/cached/check-mk-agent.deb"), + "lowpkg.bin_pkg_info": MagicMock( + return_value={"name": "check-mk-agent", "version": "1.0.0"} + ), + }, + ), patch("salt.states.pkg.os.path.exists", return_value=True): + result = pkg._find_install_targets( + name="check-mk-agent", + sources=[{"check-mk-agent": source}], + allow_updates=True, + saltenv="base", + ) + + # When nothing needs to be installed, _find_install_targets returns a dict + # (result True). If allow_updates were ignored for sources, the newer + # installed version would be flagged for reinstall and a tuple of targets + # would be returned instead. + assert isinstance(result, dict) + assert result["result"] is True + assert result["changes"] == {} + + @pytest.mark.parametrize("action", ["removed", "purged"]) def test_removed_purged_with_changes_test_true(list_pkgs, action): """ From 2a624e9cc6e70b5b69e3640a6cfaf7cc264fe71f Mon Sep 17 00:00:00 2001 From: "Gary T. Giesen" Date: Sun, 5 Jul 2026 23:18:25 -0400 Subject: [PATCH 2/2] Add direct and inverse regression tests for pkg.installed sources allow_updates The direct test already existed in this branch and calls _find_install_targets with sources and allow_updates=True, the exact kwarg pkg.installed forwards (kwargs["allow_updates"] = allow_updates). This adds the two inverse guards: with allow_updates left at its default of False a newer installed version must still be targeted for downgrade to the source package's version, and with allow_updates=True an older installed version must still be targeted for upgrade, so the fix cannot silently widen into ignoring version mismatches for sources installs. Claude-Session: https://claude.ai/code/session_01MF2AuQNhBZg4HDt1x6xxCu --- tests/pytests/unit/states/test_pkg.py | 82 +++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/tests/pytests/unit/states/test_pkg.py b/tests/pytests/unit/states/test_pkg.py index 099fd52fd2e2..35c8c8433b2c 100644 --- a/tests/pytests/unit/states/test_pkg.py +++ b/tests/pytests/unit/states/test_pkg.py @@ -644,6 +644,88 @@ def _list_pkgs(**kwargs): assert result["changes"] == {} +def test_installed_sources_without_allow_updates_still_downgrades_35385(): + """ + Inverse of the #35385 fix, guarding against overcorrection: with + ``sources`` and ``allow_updates`` left at its default (False), a newer + installed version must STILL be targeted for reinstall/downgrade to the + version reported by the source package. The fix must only change + behaviour when allow_updates is explicitly enabled. + """ + source = "salt://server/check-mk-agent.deb" + + def _list_pkgs(**kwargs): + if kwargs.get("purge_desired"): + return {} + return {"check-mk-agent": ["2.0.0"]} + + with patch.dict( + pkg.__salt__, + { + "pkg.list_pkgs": MagicMock(side_effect=_list_pkgs), + "pkg_resource.pack_sources": MagicMock( + return_value={"check-mk-agent": source} + ), + "cp.cache_file": MagicMock(return_value="/cached/check-mk-agent.deb"), + "lowpkg.bin_pkg_info": MagicMock( + return_value={"name": "check-mk-agent", "version": "1.0.0"} + ), + }, + ), patch("salt.states.pkg.os.path.exists", return_value=True): + # allow_updates=False is what pkg.installed passes in kwargs by + # default (kwargs["allow_updates"] = allow_updates) + result = pkg._find_install_targets( + name="check-mk-agent", + sources=[{"check-mk-agent": source}], + allow_updates=False, + saltenv="base", + ) + + # A tuple means targets were found; targets is the second element + assert isinstance(result, tuple) + targets = result[1] + assert targets == {"check-mk-agent": source} + + +def test_installed_sources_allow_updates_still_upgrades_35385(): + """ + Inverse of the #35385 fix, guarding against overcorrection: + ``allow_updates=True`` must only tolerate a NEWER installed version. When + the installed version is older than the one reported by the source + package, the package must still be targeted for installation. + """ + source = "salt://server/check-mk-agent.deb" + + def _list_pkgs(**kwargs): + if kwargs.get("purge_desired"): + return {} + return {"check-mk-agent": ["1.0.0"]} + + with patch.dict( + pkg.__salt__, + { + "pkg.list_pkgs": MagicMock(side_effect=_list_pkgs), + "pkg_resource.pack_sources": MagicMock( + return_value={"check-mk-agent": source} + ), + "cp.cache_file": MagicMock(return_value="/cached/check-mk-agent.deb"), + "lowpkg.bin_pkg_info": MagicMock( + return_value={"name": "check-mk-agent", "version": "2.0.0"} + ), + }, + ), patch("salt.states.pkg.os.path.exists", return_value=True): + result = pkg._find_install_targets( + name="check-mk-agent", + sources=[{"check-mk-agent": source}], + allow_updates=True, + saltenv="base", + ) + + assert isinstance(result, tuple) + targets = result[1] + assert targets == {"check-mk-agent": source} + + @pytest.mark.parametrize("action", ["removed", "purged"]) def test_removed_purged_with_changes_test_true(list_pkgs, action): """