Skip to content

Commit 4972449

Browse files
committed
Add tool install --missing and tool upgrade --all
1 parent 545b49a commit 4972449

4 files changed

Lines changed: 254 additions & 11 deletions

File tree

commodore/cli/tool.py

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,20 @@ def tool_list(config: Config, verbose: int, github_token: str, version_check: bo
5858
help="A version to install for the requested tool. "
5959
+ "By default, the latest version is installed.",
6060
)
61-
@click.argument("tool")
61+
@click.option(
62+
"--missing",
63+
is_flag=True,
64+
default=False,
65+
help="Install the latest version for all currently unmanaged tools",
66+
)
67+
@click.argument("tool", required=False, default="")
6268
def tool_install(
63-
config: Config, verbose: int, tool: str, version: Optional[str], github_token: str
69+
config: Config,
70+
verbose: int,
71+
tool: str,
72+
version: Optional[str],
73+
github_token: str,
74+
missing: bool,
6475
):
6576
"""Install one of the required tools in `$XDG_CACHE_DIR/commodore/tools`.
6677
@@ -76,7 +87,21 @@ def tool_install(
7687
"""
7788
config.update_verbosity(verbose)
7889
config.github_token = github_token
79-
tools.install_tool(config, tool, version)
90+
91+
if not tool and not missing or tool and missing:
92+
raise click.ClickException(
93+
"`commodore install tool` expects to be called with either a tool name or the `--missing` flag."
94+
)
95+
if missing and version:
96+
click.secho(
97+
"Flag `--version` has no effect when calling the command with `--missing`.",
98+
fg="yellow",
99+
)
100+
101+
if missing:
102+
tools.install_missing_tools(config)
103+
else:
104+
tools.install_tool(config, tool, version)
80105

81106

82107
@tool_group.command(name="upgrade", short_help="Upgrade external tools")
@@ -90,9 +115,20 @@ def tool_install(
90115
help="A version to upgrade (or downgrade) to for the requested tool. "
91116
+ "By default, the tool is upgraded to the latest version.",
92117
)
93-
@click.argument("tool")
118+
@click.option(
119+
"--all",
120+
is_flag=True,
121+
default=False,
122+
help="Upgrade all currently managed tools to their latest versions",
123+
)
124+
@click.argument("tool", required=False, default="")
94125
def tool_upgrade(
95-
config: Config, verbose: int, tool: str, version: Optional[str], github_token: str
126+
config: Config,
127+
verbose: int,
128+
tool: str,
129+
version: Optional[str],
130+
github_token: str,
131+
all: bool,
96132
):
97133
"""Upgrade (or downgrade) one of the required tools in `$XDG_CACHE_DIR/commodore/tools`.
98134
@@ -109,4 +145,17 @@ def tool_upgrade(
109145
"""
110146
config.update_verbosity(verbose)
111147
config.github_token = github_token
112-
tools.upgrade_tool(config, tool, version)
148+
149+
if not tool and not all or tool and all:
150+
raise click.ClickException(
151+
"`commodore upgrade tool` expects to be called with either a tool name or the `--all` flag."
152+
)
153+
if all and version:
154+
click.secho(
155+
"Flag `--version` has no effect when calling the command with `--all`.",
156+
fg="yellow",
157+
)
158+
if all:
159+
tools.upgrade_all_tools(config)
160+
else:
161+
tools.upgrade_tool(config, tool, version)

commodore/tools.py

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -304,15 +304,25 @@ def install_tool(config: Config, tool: str, version: Optional[str]):
304304
do_install(config, tool, version)
305305

306306

307-
def upgrade_tool(config: Config, tool: str, version: Optional[str]):
308-
check_known(tool)
309-
if tool not in config.managed_tools:
307+
def install_missing_tools(config: Config):
308+
MANAGED_TOOLS_PATH.mkdir(parents=True, exist_ok=True)
309+
missing_tools = set(REQUIRED_TOOLS) - set(config.managed_tools.keys())
310+
if len(missing_tools) == 0:
310311
click.echo(
311-
f"Tool {tool} not installed yet. "
312-
+ f"Use `commodore tool install {tool}` to install tools."
312+
"All required tools are already managed by Commodore.\n\n"
313+
+ "Use `commodore tool upgrade --all` to upgrade all tools to their latest versions."
313314
)
314315
return
315316

317+
for tool in sorted(REQUIRED_TOOLS):
318+
if tool in config.managed_tools:
319+
click.secho(f"Tool {tool} already managed, skipping...", fg="yellow")
320+
continue
321+
click.secho(f"Installing tool {tool}", bold=True)
322+
do_install(config, tool, None)
323+
324+
325+
def do_upgrade(config: Config, tool: str, version: Optional[str]):
316326
# The kustomize install script is unhappy if the kustomize binary is already
317327
# present in the install directory. To keep it simple, we unlink the old
318328
# tool binary for all tools when doing an upgrade (note that this generates
@@ -326,3 +336,31 @@ def upgrade_tool(config: Config, tool: str, version: Optional[str]):
326336
if tool_path.parent == MANAGED_TOOLS_PATH:
327337
tool_path.unlink()
328338
do_install(config, tool, version)
339+
340+
341+
def upgrade_tool(config: Config, tool: str, version: Optional[str]):
342+
check_known(tool)
343+
if tool not in config.managed_tools:
344+
click.echo(
345+
f"Tool {tool} not installed yet. "
346+
+ f"Use `commodore tool install {tool}` to install tools."
347+
)
348+
return
349+
350+
do_upgrade(config, tool, version)
351+
352+
353+
def upgrade_all_tools(config: Config):
354+
if len(config.managed_tools) == 0:
355+
click.echo(
356+
"No tools managed by Commodore yet.\n\n"
357+
+ "Use `commodore tool install --missing` to install the latest version for all required tools."
358+
)
359+
return
360+
361+
for tool in sorted(REQUIRED_TOOLS):
362+
if tool not in config.managed_tools:
363+
click.secho(f"Tool {tool} not managed, skipping...", fg="yellow")
364+
continue
365+
click.secho(f"Upgrading tool {tool}", bold=True)
366+
do_upgrade(config, tool, None)

tests/test_cli_tool.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,42 @@ def mock_install(_config: Config, mtool: str, mversion: Optional[str]):
6464
mock_load_state.assert_called_once()
6565

6666

67+
@patch("commodore.tools.install_missing_tools")
68+
@patch("commodore.tools.load_state")
69+
@pytest.mark.parametrize(
70+
"args,exit_code,output",
71+
[
72+
([], 0, ""),
73+
(
74+
["--version", "1.2.3"],
75+
0,
76+
"Flag `--version` has no effect when calling the command with `--missing`",
77+
),
78+
(
79+
["jb"],
80+
1,
81+
"`commodore install tool` expects to be called with either a tool name or the `--missing` flag.",
82+
),
83+
],
84+
)
85+
def test_tool_install_missing(
86+
mock_load_state,
87+
mock_install_missing_tools,
88+
cli_runner: RunnerFunc,
89+
args: list[str],
90+
exit_code: int,
91+
output: str,
92+
):
93+
result = cli_runner(["tool", "install", "--missing"] + args)
94+
assert result.exit_code == exit_code
95+
assert output in result.output
96+
if exit_code == 0:
97+
mock_install_missing_tools.assert_called_once()
98+
else:
99+
mock_install_missing_tools.assert_not_called()
100+
mock_load_state.assert_called_once()
101+
102+
67103
@patch("commodore.tools.upgrade_tool")
68104
@patch("commodore.tools.load_state")
69105
@pytest.mark.parametrize(
@@ -90,3 +126,39 @@ def mock_upgrade(_config: Config, mtool: str, mversion: Optional[str]):
90126
result = cli_runner(["tool", "upgrade", tool] + args)
91127
assert result.exit_code == 0
92128
mock_load_state.assert_called_once()
129+
130+
131+
@patch("commodore.tools.upgrade_all_tools")
132+
@patch("commodore.tools.load_state")
133+
@pytest.mark.parametrize(
134+
"args,exit_code,output",
135+
[
136+
([], 0, ""),
137+
(
138+
["--version", "1.2.3"],
139+
0,
140+
"Flag `--version` has no effect when calling the command with `--all`",
141+
),
142+
(
143+
["jb"],
144+
1,
145+
"`commodore upgrade tool` expects to be called with either a tool name or the `--all` flag.",
146+
),
147+
],
148+
)
149+
def test_tool_upgrade_all(
150+
mock_load_state,
151+
mock_upgrade_all_tools,
152+
cli_runner: RunnerFunc,
153+
args: list[str],
154+
exit_code: int,
155+
output: str,
156+
):
157+
result = cli_runner(["tool", "upgrade", "--all"] + args)
158+
assert result.exit_code == exit_code
159+
assert output in result.output
160+
if exit_code == 0:
161+
mock_upgrade_all_tools.assert_called_once()
162+
else:
163+
mock_upgrade_all_tools.assert_not_called()
164+
mock_load_state.assert_called_once()

tests/test_tools.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,55 @@ def test_install_kustomize(config: Config, capfd, tmp_path, github_token):
468468
assert outlines[4] == f"GITHUB_TOKEN={github_token}"
469469

470470

471+
@patch.object(tools, "do_install")
472+
@pytest.mark.parametrize(
473+
"managed,output",
474+
[
475+
(
476+
{
477+
"helm": "2025-07-11T11:04:40",
478+
"jb": "2025-07-11T10:58:11",
479+
"kustomize": "2025-07-11T11:05:05",
480+
},
481+
"All required tools are already managed by Commodore.\n\n"
482+
+ "Use `commodore tool upgrade --all` to upgrade all tools "
483+
+ "to their latest versions.\n",
484+
),
485+
(
486+
{"jb": "2025-07-11T10:58:11"},
487+
"Installing tool helm\n"
488+
+ "Tool jb already managed, skipping...\n"
489+
+ "Installing tool kustomize\n",
490+
),
491+
],
492+
)
493+
def test_install_missing_tools(
494+
mock_do_install: MagicMock,
495+
config: Config,
496+
capsys,
497+
managed: dict[str, str],
498+
output: str,
499+
):
500+
config.managed_tools = managed
501+
502+
tools.install_missing_tools(config)
503+
504+
assert mock_do_install.call_count == len(tools.REQUIRED_TOOLS) - len(managed)
505+
506+
out, _ = capsys.readouterr()
507+
assert out == output
508+
509+
510+
def test_install_script_invalid_version(config: Config):
511+
with pytest.raises(ValueError) as exc:
512+
tools.install_script(config, "jb", "v0.6.3")
513+
514+
assert (
515+
str(exc.value)
516+
== "Function install_script() expects parameter `version` to not be prefixed with 'v'."
517+
)
518+
519+
471520
def test_upgrade_tool_not_installed(config: Config, capsys):
472521
config.managed_tools = {}
473522
tools.upgrade_tool(config, "jb", None)
@@ -510,3 +559,38 @@ def do_inst(_config: Config, tool: str, version: Optional[str]):
510559
tools.upgrade_tool(config, "jb", None)
511560

512561
assert jb_file.exists() != managed_jb
562+
563+
564+
@patch.object(tools, "do_upgrade")
565+
@pytest.mark.parametrize(
566+
"managed,output",
567+
[
568+
(
569+
{},
570+
"No tools managed by Commodore yet.\n\n"
571+
+ "Use `commodore tool install --missing` to install the latest "
572+
+ "version for all required tools.\n",
573+
),
574+
(
575+
{"jb": "2025-07-11T10:58:11"},
576+
"Tool helm not managed, skipping...\n"
577+
+ "Upgrading tool jb\n"
578+
+ "Tool kustomize not managed, skipping...\n",
579+
),
580+
],
581+
)
582+
def test_upgrade_all_tools(
583+
mock_do_upgrade: MagicMock,
584+
config: Config,
585+
capsys,
586+
managed: dict[str, str],
587+
output: str,
588+
):
589+
config.managed_tools = managed
590+
591+
tools.upgrade_all_tools(config)
592+
593+
assert mock_do_upgrade.call_count == len(managed)
594+
595+
out, _ = capsys.readouterr()
596+
assert out == output

0 commit comments

Comments
 (0)