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
1 change: 1 addition & 0 deletions changelog/68210.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed ``pkg.group_installed`` reporting failure on RPM-based systems when a package group's default or optional members are not available in any enabled repository. The state now only considers mandatory group members and explicitly requested ``include`` packages when checking for install failures, matching the behavior of ``yum/dnf group install`` (which reports "No match for group package" but still exits 0).
15 changes: 13 additions & 2 deletions salt/states/pkg.py
Original file line number Diff line number Diff line change
Expand Up @@ -3437,7 +3437,8 @@ def group_installed(name, skip=None, include=None, **kwargs):
)
return ret

targets = diff["mandatory"]["not installed"]
mandatory_targets = list(diff["mandatory"]["not installed"])
targets = list(mandatory_targets)
targets.extend([x for x in diff["default"]["not installed"] if x not in skip])
targets.extend(include)

Expand Down Expand Up @@ -3478,7 +3479,17 @@ def group_installed(name, skip=None, include=None, **kwargs):
)
return ret

failed = [x for x in targets if x not in __salt__["pkg.list_pkgs"](**kwargs)]
# Only flag a failure when a *mandatory* group member is missing after
# install, or when an explicitly user-requested ``include`` package is
# missing. Default/optional group members that the package manager could
# not install (e.g. arch-specific subpackages not present in any enabled
# repo) match the underlying ``yum/dnf group install`` behavior, which
# reports "No match for group package <X>" and still exits 0. Treating
# those as state failures contradicts the package manager's own result
# and surfaces as a spurious red state run -- see #68210.
required = list(mandatory_targets) + list(include)
installed_pkgs = __salt__["pkg.list_pkgs"](**kwargs)
failed = [x for x in required if x not in installed_pkgs]
if failed:
ret["comment"] = "Failed to install the following packages: {}".format(
", ".join(failed)
Expand Down
70 changes: 70 additions & 0 deletions tests/pytests/unit/states/test_pkg.py
Original file line number Diff line number Diff line change
Expand Up @@ -1143,6 +1143,76 @@ def test_pacmanpkg_group_installed_with_repo_options(list_pkgs):
assert ret["comment"] == "Repo options are not supported on this platform"


def test_group_installed_unavailable_optional_member_68210():
"""
Regression test for #68210.

pkg.group_installed must not fail when a group's default/optional member
does not exist in any enabled repository. dnf/yum itself reports such
"No match for group package" cases as success, and they should not
flip the state's result to False.
"""
name = "Performance Tools"
# The group declares pcp-pmda-kvm as a default member, but the repo
# does not provide it on this arch. yum/dnf installs the rest and
# exits 0; pkg.install returns the actually-installed pkgs only.
diff = {
"mandatory": {"installed": [], "not installed": []},
"default": {
"installed": [],
"not installed": ["perf", "pcp-pmda-kvm"],
},
"optional": {"installed": [], "not installed": []},
"conditional": {"installed": [], "not installed": []},
}
group_diff_mock = MagicMock(return_value=diff)
install_mock = MagicMock(return_value={"perf": {"old": "", "new": "6.12.0"}})
list_pkgs_after = MagicMock(return_value={"perf": "6.12.0"})

salt_dict = {
"pkg.group_diff": group_diff_mock,
"pkg.install": install_mock,
"pkg.list_pkgs": list_pkgs_after,
}

with patch.dict(pkg.__salt__, salt_dict):
ret = pkg.group_installed(name)

assert ret["result"] is True, ret
assert ret["changes"] == {"perf": {"old": "", "new": "6.12.0"}}
assert "Failed to install" not in ret["comment"]


def test_group_installed_mandatory_member_missing_still_fails_68210():
"""
Companion to test_group_installed_unavailable_optional_member_68210:
if a *mandatory* group member fails to install, the state must still
fail. Only default/optional members are forgiven when missing.
"""
name = "Critical Group"
diff = {
"mandatory": {"installed": [], "not installed": ["required-pkg"]},
"default": {"installed": [], "not installed": []},
"optional": {"installed": [], "not installed": []},
"conditional": {"installed": [], "not installed": []},
}
group_diff_mock = MagicMock(return_value=diff)
install_mock = MagicMock(return_value={})
list_pkgs_after = MagicMock(return_value={})

salt_dict = {
"pkg.group_diff": group_diff_mock,
"pkg.install": install_mock,
"pkg.list_pkgs": list_pkgs_after,
}

with patch.dict(pkg.__salt__, salt_dict):
ret = pkg.group_installed(name)

assert ret["result"] is False, ret
assert "required-pkg" in ret["comment"]


def test_latest():
"""
Test pkg.latest
Expand Down
Loading