Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use --json=v2 for listing macOS brew casks #59439

Merged
Merged
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/59439.changed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Improve support for listing macOS brew casks
73 changes: 34 additions & 39 deletions salt/modules/mac_brew_pkg.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@
"""

import copy
import functools
import logging
import re

import salt.utils.data
import salt.utils.functools
Expand Down Expand Up @@ -181,38 +179,15 @@ def list_pkgs(versions_as_list=False, **kwargs):
for name, version in combinations:
__salt__["pkg_resource.add_pkg"](ret, name, version)

# Grab packages from brew cask, if available.
# Brew Cask doesn't provide a JSON interface, must be parsed the old way.
try:
out = _call_brew("list", "--cask", "--versions")["stdout"]

for line in out.splitlines():
try:
name_and_versions = line.split(" ")
pkg_name = name_and_versions[0]

# Get cask namespace
match = re.search(
r"^From: .*/(.+?)/homebrew-(.+?)/.*$",
_call_brew("info", "--cask", pkg_name)["stdout"],
re.MULTILINE,
)
if match:
namespace = "/".join(
(match.group(1).lower(), match.group(2).lower())
)
else:
namespace = "homebrew/cask"

name = "/".join((namespace, pkg_name))
installed_versions = name_and_versions[1:]
key_func = functools.cmp_to_key(salt.utils.versions.version_cmp)
newest_version = sorted(installed_versions, key=key_func).pop()
except ValueError:
continue
__salt__["pkg_resource.add_pkg"](ret, name, newest_version)
except CommandExecutionError:
pass
for package in package_info["casks"]:
version = package["installed"]
names = {package["full_token"], package["token"]}
# The following name is appended to maintain backward compatibility
# with old salt formulas. Since full_token and token are the same
# for official taps (homebrew/*).
names.add("/".join([package["tap"], package["token"]]))
for name in names:
__salt__["pkg_resource.add_pkg"](ret, name, version)

__salt__["pkg_resource.sort_pkglist"](ret)
__context__["pkg.list_pkgs"] = copy.deepcopy(ret)
Expand Down Expand Up @@ -372,7 +347,21 @@ def _info(*pkgs):
log.error("Failed to get info about packages: %s", " ".join(pkgs))
return {}
output = salt.utils.json.loads(brew_result["stdout"])
return dict(zip(pkgs, output["formulae"]))

meta_info = {"formulae": ["name", "full_name"], "casks": ["token", "full_token"]}

pkgs_info = dict()
for tap, keys in meta_info.items():
data = output[tap]
if len(data) == 0:
continue

for _pkg in data:
for key in keys:
if _pkg[key] in pkgs:
pkgs_info[_pkg[key]] = _pkg

return pkgs_info


def install(name=None, pkgs=None, taps=None, options=None, **kwargs):
Expand Down Expand Up @@ -484,7 +473,7 @@ def install(name=None, pkgs=None, taps=None, options=None, **kwargs):
return ret


def list_upgrades(refresh=True, **kwargs): # pylint: disable=W0613
def list_upgrades(refresh=True, include_casks=False, **kwargs): # pylint: disable=W0613
"""
Check whether or not an upgrade is available for all packages

Expand All @@ -501,15 +490,21 @@ def list_upgrades(refresh=True, **kwargs): # pylint: disable=W0613
ret = {}

try:
data = salt.utils.json.loads(res["stdout"])["formulae"]
data = salt.utils.json.loads(res["stdout"])
except ValueError as err:
msg = 'unable to interpret output from "brew outdated": {}'.format(err)
log.error(msg)
raise CommandExecutionError(msg)

for pkg in data:
for pkg in data["formulae"]:
# current means latest available to brew
ret[pkg["name"]] = pkg["current_version"]

if include_casks:
for pkg in data["casks"]:
# current means latest available to brew
ret[pkg["name"]] = pkg["current_version"]

return ret


Expand All @@ -523,7 +518,7 @@ def upgrade_available(pkg, **kwargs):

salt '*' pkg.upgrade_available <package name>
"""
return pkg in list_upgrades()
return pkg in list_upgrades(**kwargs)


def upgrade(refresh=True, **kwargs):
Expand Down
200 changes: 151 additions & 49 deletions tests/pytests/unit/modules/test_mac_brew_pkg.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ def custom_call_brew(*cmd, failhard=True):
"outdated": false,
"sha256": "4963f503c1e47bfa0f8bdbbbe5694d6a7242d298fb44ff68af80d42f1eaebaf9",
"token": "day-o",
"full_token": "day-o",
"tap": "homebrew/cask",
"url": "https://shauninman.com/assets/downloads/Day-3.0.zip",
"version": "3.0.1"
},
Expand Down Expand Up @@ -109,6 +111,8 @@ def custom_call_brew(*cmd, failhard=True):
"outdated": false,
"sha256": "9ed73844838bddf797eadf37e5f7da3771308c3f74d38cd422c18eebaaa8f6b9",
"token": "iterm2",
"full_token": "custom/tap/iterm2",
"tap": "custom/tap",
"url": "https://iterm2.com/downloads/stable/iTerm2-3_4_3.zip",
"version": "3.4.3"
}
Expand Down Expand Up @@ -302,54 +306,6 @@ def custom_call_brew(*cmd, failhard=True):
"stderr": "",
"retcode": 0,
}
elif cmd == ("list", "--cask", "--versions"):
result = {
"stdout": "day-o 3.0.1\niterm2 3.4.3",
"stderr": "",
"retcode": 0,
}
elif cmd == ("info", "--cask", "iterm2"):
result = {
"stdout": textwrap.dedent(
"""\
iterm2: 3.4.3 (auto_updates)
https://www.iterm2.com/
/usr/local/Caskroom/iterm2/3.4.3 (119B)
From: https://github.com/Homebrew/homebrew-cask/blob/HEAD/Casks/iterm2.rb
==> Name
iTerm2
==> Description
Terminal emulator as alternative to Apple's Terminal app
==> Artifacts
iTerm.app (App)
==> Analytics
install: 18,869 (30 days), 61,676 (90 days), 233,825 (365 days)
"""
),
"stderr": "",
"retcode": 0,
}
elif cmd == ("info", "--cask", "day-o"):
result = {
"stdout": textwrap.dedent(
"""\
day-o: 3.0.1
https://shauninman.com/archive/2020/04/08/day_o_mac_menu_bar_clock_for_catalina
/usr/local/Caskroom/day-o/3.0.1 (7.3KB)
From: https://github.com/Homebrew/homebrew-cask/blob/HEAD/Casks/day-o.rb
==> Name
Day-O
==> Description
None
==> Artifacts
Day-3.0/Day-O.app (App)
==> Analytics
install: 30 (30 days), 96 (90 days), 525 (365 days)
"""
),
"stderr": "",
"retcode": "",
}

return result

Expand Down Expand Up @@ -469,7 +425,9 @@ def test_list_pkgs_homebrew_cask_pakages():
"""
expected_pkgs = {
"homebrew/cask/day-o": "3.0.1",
"homebrew/cask/iterm2": "3.4.3",
"day-o": "3.0.1",
"custom/tap/iterm2": "3.4.3",
"iterm2": "3.4.3",
"jq": "1.6",
"xz": "5.2.5",
}
Expand Down Expand Up @@ -802,3 +760,147 @@ def test_unhold_not_pinned():
},
):
assert mac_brew.unhold("foo") == _expected

def test_info_installed(self):
"""
Tests info_installed method
"""
mock_user = MagicMock(return_value="foo")
mock_cmd = MagicMock(return_value="")
mock_cmd_all = MagicMock(
return_value={
"pid": 12345,
"retcode": 0,
"stderr": "",
"stdout": textwrap.dedent(
"""\
{
"formulae": [
{
"name": "salt",
"full_name": "cdalvaro/tap/salt",
"tap": "cdalvaro/tap",
"aliases": []
},
{
"name": "vim",
"full_name": "vim",
"tap": "homebrew/core",
"aliases": []
}
],
"casks": [
{
"token": "visual-studio-code",
"full_token": "visual-studio-code",
"tap": "homebrew/cask",
"name": [
"MicrosoftVisualStudioCode",
"VSCode"
]
}
]
}
"""
),
}
)
_expected = {
"cdalvaro/tap/salt": {
"name": "salt",
"full_name": "cdalvaro/tap/salt",
"tap": "cdalvaro/tap",
"aliases": [],
},
"vim": {
"name": "vim",
"full_name": "vim",
"tap": "homebrew/core",
"aliases": [],
},
"visual-studio-code": {
"token": "visual-studio-code",
"full_token": "visual-studio-code",
"tap": "homebrew/cask",
"name": ["MicrosoftVisualStudioCode", "VSCode"],
},
}

with patch("salt.modules.mac_brew_pkg.list_pkgs", return_value={}), patch(
"salt.modules.mac_brew_pkg._list_pinned", return_value=["foo"]
), patch.dict(
mac_brew.__salt__,
{
"file.get_user": mock_user,
"cmd.run_all": mock_cmd_all,
"cmd.run": mock_cmd,
},
):
self.assertEqual(
mac_brew.info_installed(
"cdalvaro/tap/salt", "vim", "visual-studio-code"
),
_expected,
)

def test_list_upgrades(self):
"""
Tests list_upgrades method
"""
mock_user = MagicMock(return_value="foo")
mock_cmd = MagicMock(return_value="")
mock_cmd_all = MagicMock(
return_value={
"pid": 12345,
"retcode": 0,
"stderr": "",
"stdout": textwrap.dedent(
"""\
{
"formulae": [
{
"name": "cmake",
"installed_versions": ["3.19.3"],
"current_version": "3.19.4",
"pinned": false,
"pinned_version": null
},
{
"name": "fzf",
"installed_versions": ["0.25.0"],
"current_version": "0.25.1",
"pinned": false,
"pinned_version": null
}
],
"casks": [
{
"name": "ksdiff",
"installed_versions": "2.2.0,122",
"current_version": "2.3.6,123-jan-18-2021"
}
]
}
"""
),
}
)
_expected = {
"cmake": "3.19.4",
"fzf": "0.25.1",
"ksdiff": "2.3.6,123-jan-18-2021",
}

with patch("salt.modules.mac_brew_pkg.list_pkgs", return_value={}), patch(
"salt.modules.mac_brew_pkg._list_pinned", return_value=["foo"]
), patch.dict(
mac_brew.__salt__,
{
"file.get_user": mock_user,
"cmd.run_all": mock_cmd_all,
"cmd.run": mock_cmd,
},
):
self.assertEqual(
mac_brew.list_upgrades(refresh=False, include_casks=True), _expected
)