Skip to content

Commit 3ce7b9c

Browse files
committed
feat(macOS): add support for trust/untrust homebrew commands
1 parent 445d03e commit 3ce7b9c

2 files changed

Lines changed: 344 additions & 0 deletions

File tree

salt/modules/mac_brew_pkg.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
# Define the module's virtual name
3434
__virtualname__ = "pkg"
3535

36+
_TRUST_TYPES = ("tap", "formula", "cask", "command")
37+
3638

3739
def __virtual__():
3840
"""
@@ -895,3 +897,156 @@ def unhold(name=None, pkgs=None, sources=None, **kwargs): # pylint: disable=W06
895897

896898

897899
unpin = unhold
900+
901+
902+
def list_trusted(type=None):
903+
"""
904+
List trusted taps, formulae, casks and commands.
905+
906+
.. versionadded:: 3008.2
907+
908+
type
909+
Filter by type. Valid values: ``tap``, ``formula``, ``cask``, ``command``.
910+
When ``None`` (default), returns a dict with all trusted items grouped by
911+
type (keys: ``taps``, ``formulae``, ``casks``, ``commands``). When a type
912+
is specified, returns a list of trusted items of that type.
913+
914+
CLI Example:
915+
916+
.. code-block:: bash
917+
918+
salt '*' pkg.list_trusted
919+
salt '*' pkg.list_trusted type=tap
920+
"""
921+
if type is not None and type not in _TRUST_TYPES:
922+
raise SaltInvocationError(
923+
f"Invalid type '{type}'. Valid values: {', '.join(_TRUST_TYPES)}"
924+
)
925+
926+
cmd = ["trust", "--json=v1"]
927+
if type is not None:
928+
cmd.append(f"--{type}")
929+
930+
result = _call_brew(*cmd)
931+
try:
932+
return salt.utils.json.loads(result["stdout"])
933+
except ValueError as err:
934+
msg = f'Unable to interpret output from "brew trust": {err}'
935+
log.error(msg)
936+
raise CommandExecutionError(msg)
937+
938+
939+
def trust(name, type=None):
940+
"""
941+
Trust a tap, formula, cask or command so Homebrew may load it when
942+
``$HOMEBREW_REQUIRE_TAP_TRUST`` is set.
943+
944+
.. versionadded:: 3008.2
945+
946+
name
947+
The name of the tap, formula, cask or command to trust. Can also be a
948+
remote URL for taps.
949+
950+
type
951+
The type of the item. Valid values: ``tap``, ``formula``, ``cask``,
952+
``command``. If not specified, Homebrew will auto-detect the type.
953+
954+
Returns ``True`` on success (including when the item was already trusted),
955+
``False`` on failure.
956+
957+
CLI Example:
958+
959+
.. code-block:: bash
960+
961+
salt '*' pkg.trust cdalvaro/tap
962+
salt '*' pkg.trust cdalvaro/tap type=tap
963+
salt '*' pkg.trust cdalvaro/tap/salt type=formula
964+
"""
965+
if type is not None and type not in _TRUST_TYPES:
966+
raise SaltInvocationError(
967+
f"Invalid type '{type}'. Valid values: {', '.join(_TRUST_TYPES)}"
968+
)
969+
970+
cmd = ["trust"]
971+
if type is not None:
972+
cmd.append(f"--{type}")
973+
cmd.append(name)
974+
975+
try:
976+
_call_brew(*cmd)
977+
except CommandExecutionError:
978+
log.error('Failed to trust "%s"', name)
979+
return False
980+
981+
return True
982+
983+
984+
def untrust(name, type=None):
985+
"""
986+
Stop trusting a tap, formula, cask or command.
987+
988+
.. versionadded:: 3008.2
989+
990+
name
991+
The name of the tap, formula, cask or command to untrust.
992+
993+
type
994+
The type of the item. Valid values: ``tap``, ``formula``, ``cask``,
995+
``command``. If not specified, Homebrew will auto-detect the type.
996+
997+
Returns ``True`` on success (including when the item was not trusted),
998+
``False`` on failure.
999+
1000+
CLI Example:
1001+
1002+
.. code-block:: bash
1003+
1004+
salt '*' pkg.untrust cdalvaro/tap
1005+
salt '*' pkg.untrust cdalvaro/tap type=tap
1006+
salt '*' pkg.untrust cdalvaro/tap/salt type=formula
1007+
"""
1008+
if type is not None and type not in _TRUST_TYPES:
1009+
raise SaltInvocationError(
1010+
f"Invalid type '{type}'. Valid values: {', '.join(_TRUST_TYPES)}"
1011+
)
1012+
1013+
cmd = ["untrust"]
1014+
if type is not None:
1015+
cmd.append(f"--{type}")
1016+
cmd.append(name)
1017+
1018+
try:
1019+
_call_brew(*cmd)
1020+
except CommandExecutionError:
1021+
log.error('Failed to untrust "%s"', name)
1022+
return False
1023+
1024+
return True
1025+
1026+
1027+
def is_trusted(name, type=None):
1028+
"""
1029+
Check whether a tap, formula, cask or command is trusted.
1030+
1031+
.. versionadded:: 3008.2
1032+
1033+
name
1034+
The name of the tap, formula, cask or command to check.
1035+
1036+
type
1037+
The type of the item. Valid values: ``tap``, ``formula``, ``cask``,
1038+
``command``. If not specified, checks across all types.
1039+
1040+
Returns ``True`` if the item is trusted, ``False`` otherwise.
1041+
1042+
CLI Example:
1043+
1044+
.. code-block:: bash
1045+
1046+
salt '*' pkg.is_trusted cdalvaro/tap
1047+
salt '*' pkg.is_trusted cdalvaro/tap type=tap
1048+
"""
1049+
trusted = list_trusted(type=type)
1050+
if isinstance(trusted, list):
1051+
return name in trusted
1052+
return any(name in items for items in trusted.values())

tests/pytests/unit/modules/test_mac_brew_pkg.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1608,3 +1608,192 @@ def test_list_upgrades_with_options():
16081608
mock_call_brew.assert_called_once_with(
16091609
"outdated", "--json=v2", "--greedy", "--fetch-HEAD"
16101610
)
1611+
1612+
1613+
# 'list_trusted' function tests
1614+
1615+
1616+
def test_list_trusted():
1617+
"""
1618+
Tests that list_trusted returns all trusted items as a dict.
1619+
"""
1620+
expected = {
1621+
"taps": ["thirdparty/foo"],
1622+
"formulae": ["thirdparty/foo/bar"],
1623+
"casks": [],
1624+
"commands": [],
1625+
}
1626+
mock_call_brew = MagicMock(
1627+
return_value={
1628+
"retcode": 0,
1629+
"stdout": '{"taps": ["thirdparty/foo"], "formulae": ["thirdparty/foo/bar"], "casks": [], "commands": []}',
1630+
"stderr": "",
1631+
}
1632+
)
1633+
with patch("salt.modules.mac_brew_pkg._call_brew", mock_call_brew):
1634+
assert mac_brew.list_trusted() == expected
1635+
mock_call_brew.assert_called_once_with("trust", "--json=v1")
1636+
1637+
1638+
def test_list_trusted_by_type():
1639+
"""
1640+
Tests that list_trusted with a type returns a list of trusted items.
1641+
"""
1642+
mock_call_brew = MagicMock(
1643+
return_value={
1644+
"retcode": 0,
1645+
"stdout": '["thirdparty/foo"]',
1646+
"stderr": "",
1647+
}
1648+
)
1649+
with patch("salt.modules.mac_brew_pkg._call_brew", mock_call_brew):
1650+
assert mac_brew.list_trusted(type="tap") == ["thirdparty/foo"]
1651+
mock_call_brew.assert_called_once_with("trust", "--json=v1", "--tap")
1652+
1653+
1654+
def test_list_trusted_invalid_type():
1655+
"""
1656+
Tests that list_trusted raises SaltInvocationError for an invalid type.
1657+
"""
1658+
with pytest.raises(
1659+
salt.exceptions.SaltInvocationError, match="Invalid type 'invalid'"
1660+
):
1661+
mac_brew.list_trusted(type="invalid")
1662+
1663+
1664+
# 'trust' function tests
1665+
1666+
1667+
def test_trust():
1668+
"""
1669+
Tests successfully trusting a tap.
1670+
"""
1671+
mock_call_brew = MagicMock(return_value={"retcode": 0, "stdout": "", "stderr": ""})
1672+
with patch("salt.modules.mac_brew_pkg._call_brew", mock_call_brew):
1673+
assert mac_brew.trust("thirdparty/foo") is True
1674+
mock_call_brew.assert_called_once_with("trust", "thirdparty/foo")
1675+
1676+
1677+
def test_trust_with_type():
1678+
"""
1679+
Tests trusting an item with an explicit type flag.
1680+
"""
1681+
mock_call_brew = MagicMock(return_value={"retcode": 0, "stdout": "", "stderr": ""})
1682+
with patch("salt.modules.mac_brew_pkg._call_brew", mock_call_brew):
1683+
assert mac_brew.trust("thirdparty/foo", type="tap") is True
1684+
mock_call_brew.assert_called_once_with("trust", "--tap", "thirdparty/foo")
1685+
1686+
1687+
def test_trust_failure():
1688+
"""
1689+
Tests that trust returns False when brew trust fails.
1690+
"""
1691+
mock_call_brew = MagicMock(side_effect=CommandExecutionError("brew failed"))
1692+
with patch("salt.modules.mac_brew_pkg._call_brew", mock_call_brew):
1693+
assert mac_brew.trust("thirdparty/foo") is False
1694+
1695+
1696+
def test_trust_invalid_type():
1697+
"""
1698+
Tests that trust raises SaltInvocationError for an invalid type.
1699+
"""
1700+
with pytest.raises(
1701+
salt.exceptions.SaltInvocationError, match="Invalid type 'invalid'"
1702+
):
1703+
mac_brew.trust("thirdparty/foo", type="invalid")
1704+
1705+
1706+
# 'untrust' function tests
1707+
1708+
1709+
def test_untrust():
1710+
"""
1711+
Tests successfully untrusting a tap.
1712+
"""
1713+
mock_call_brew = MagicMock(return_value={"retcode": 0, "stdout": "", "stderr": ""})
1714+
with patch("salt.modules.mac_brew_pkg._call_brew", mock_call_brew):
1715+
assert mac_brew.untrust("thirdparty/foo") is True
1716+
mock_call_brew.assert_called_once_with("untrust", "thirdparty/foo")
1717+
1718+
1719+
def test_untrust_with_type():
1720+
"""
1721+
Tests untrusting an item with an explicit type flag.
1722+
"""
1723+
mock_call_brew = MagicMock(return_value={"retcode": 0, "stdout": "", "stderr": ""})
1724+
with patch("salt.modules.mac_brew_pkg._call_brew", mock_call_brew):
1725+
assert mac_brew.untrust("thirdparty/foo/bar", type="formula") is True
1726+
mock_call_brew.assert_called_once_with(
1727+
"untrust", "--formula", "thirdparty/foo/bar"
1728+
)
1729+
1730+
1731+
def test_untrust_failure():
1732+
"""
1733+
Tests that untrust returns False when brew untrust fails.
1734+
"""
1735+
mock_call_brew = MagicMock(side_effect=CommandExecutionError("brew failed"))
1736+
with patch("salt.modules.mac_brew_pkg._call_brew", mock_call_brew):
1737+
assert mac_brew.untrust("thirdparty/foo") is False
1738+
1739+
1740+
def test_untrust_invalid_type():
1741+
"""
1742+
Tests that untrust raises SaltInvocationError for an invalid type.
1743+
"""
1744+
with pytest.raises(
1745+
salt.exceptions.SaltInvocationError, match="Invalid type 'invalid'"
1746+
):
1747+
mac_brew.untrust("thirdparty/foo", type="invalid")
1748+
1749+
1750+
# 'is_trusted' function tests
1751+
1752+
1753+
def test_is_trusted_found():
1754+
"""
1755+
Tests is_trusted returns True when the item is in the trusted list.
1756+
"""
1757+
trusted = {
1758+
"taps": ["thirdparty/foo"],
1759+
"formulae": [],
1760+
"casks": [],
1761+
"commands": [],
1762+
}
1763+
with patch("salt.modules.mac_brew_pkg.list_trusted", return_value=trusted):
1764+
assert mac_brew.is_trusted("thirdparty/foo") is True
1765+
1766+
1767+
def test_is_trusted_not_found():
1768+
"""
1769+
Tests is_trusted returns False when the item is not trusted.
1770+
"""
1771+
trusted = {
1772+
"taps": [],
1773+
"formulae": [],
1774+
"casks": [],
1775+
"commands": [],
1776+
}
1777+
with patch("salt.modules.mac_brew_pkg.list_trusted", return_value=trusted):
1778+
assert mac_brew.is_trusted("thirdparty/foo") is False
1779+
1780+
1781+
def test_is_trusted_with_type():
1782+
"""
1783+
Tests is_trusted with a type filter delegates to list_trusted with that type.
1784+
"""
1785+
with patch(
1786+
"salt.modules.mac_brew_pkg.list_trusted", return_value=["thirdparty/foo"]
1787+
) as mock_list:
1788+
assert mac_brew.is_trusted("thirdparty/foo", type="tap") is True
1789+
mock_list.assert_called_once_with(type="tap")
1790+
1791+
1792+
def test_is_trusted_with_type_not_found():
1793+
"""
1794+
Tests is_trusted with a type filter returns False when not in list.
1795+
"""
1796+
with patch(
1797+
"salt.modules.mac_brew_pkg.list_trusted", return_value=["other/tap"]
1798+
):
1799+
assert mac_brew.is_trusted("thirdparty/foo", type="tap") is False

0 commit comments

Comments
 (0)