Skip to content

Commit f0c037d

Browse files
committed
Add tool install --missing and tool upgrade --all
1 parent af30ca7 commit f0c037d

6 files changed

Lines changed: 281 additions & 12 deletions

File tree

commodore/cli/tool.py

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,21 @@ def tool_list(config: Config, verbose: int, github_token: str, version_check: bo
6363
help="A version to install for the requested tool. "
6464
+ "By default, the latest version is installed.",
6565
)
66-
@click.argument("tool")
66+
@click.option(
67+
"--missing",
68+
is_flag=True,
69+
default=False,
70+
help="Install the latest version for all currently unmanaged tools. "
71+
+ "This flag and providing a tool name on the command line are mutually exclusive.",
72+
)
73+
@click.argument("tool", required=False, default="")
6774
def tool_install(
68-
config: Config, verbose: int, tool: str, version: Optional[str], github_token: str
75+
config: Config,
76+
verbose: int,
77+
tool: str,
78+
version: Optional[str],
79+
github_token: str,
80+
missing: bool,
6981
):
7082
"""Install one of the required tools in `$XDG_CACHE_DIR/commodore/tools`.
7183
@@ -78,10 +90,28 @@ def tool_install(
7890
7991
Optionally, the command accepts a tool version to install. The command
8092
accepts versions prefixed with "v" and unprefixed versions.
93+
94+
Alternatively, the command can install the latest version for all required
95+
tools which aren't managed by Commodore yet by passing flag `--missing`.
96+
This flag is mutually exclusive with providing a tool name.
8197
"""
8298
config.update_verbosity(verbose)
8399
config.github_token = github_token
84-
tools.install_tool(config, tool, version)
100+
101+
if not tool and not missing or tool and missing:
102+
raise click.ClickException(
103+
"`commodore tool install` expects to be called with either a tool name or the `--missing` flag."
104+
)
105+
if missing and version:
106+
click.secho(
107+
"Flag `--version` has no effect when calling the command with `--missing`.",
108+
fg="yellow",
109+
)
110+
111+
if missing:
112+
tools.install_missing_tools(config)
113+
else:
114+
tools.install_tool(config, tool, version)
85115

86116

87117
@tool_group.command(name="upgrade", short_help="Upgrade external tools")
@@ -95,9 +125,21 @@ def tool_install(
95125
help="A version to upgrade (or downgrade) to for the requested tool. "
96126
+ "By default, the tool is upgraded to the latest version.",
97127
)
98-
@click.argument("tool")
128+
@click.option(
129+
"--all",
130+
is_flag=True,
131+
default=False,
132+
help="Upgrade all currently managed tools to their latest versions. "
133+
+ "This flag and providing a tool name on the command line are mutually exclusive.",
134+
)
135+
@click.argument("tool", required=False, default="")
99136
def tool_upgrade(
100-
config: Config, verbose: int, tool: str, version: Optional[str], github_token: str
137+
config: Config,
138+
verbose: int,
139+
tool: str,
140+
version: Optional[str],
141+
github_token: str,
142+
all: bool,
101143
):
102144
"""Upgrade (or downgrade) one of the required tools in `$XDG_CACHE_DIR/commodore/tools`.
103145
@@ -111,7 +153,24 @@ def tool_upgrade(
111153
112154
Optionally, the command accepts a tool version to upgrade (or downgrade) to.
113155
The command accepts versions prefixed with "v" and unprefixed versions.
156+
157+
Alternatively, the command can upgrade all managed tools to their latest
158+
versions by passing flag `--all`. This flag is mutually exclusive with
159+
providing a tool name.
114160
"""
115161
config.update_verbosity(verbose)
116162
config.github_token = github_token
117-
tools.upgrade_tool(config, tool, version)
163+
164+
if not tool and not all or tool and all:
165+
raise click.ClickException(
166+
"`commodore tool upgrade` expects to be called with either a tool name or the `--all` flag."
167+
)
168+
if all and version:
169+
click.secho(
170+
"Flag `--version` has no effect when calling the command with `--all`.",
171+
fg="yellow",
172+
)
173+
if all:
174+
tools.upgrade_all_tools(config)
175+
else:
176+
tools.upgrade_tool(config, tool, version)

commodore/tools.py

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

292292

293-
def upgrade_tool(config: Config, tool: str, version: Optional[str]):
294-
check_known(tool)
295-
if tool not in config.managed_tools:
293+
def install_missing_tools(config: Config):
294+
MANAGED_TOOLS_PATH.mkdir(parents=True, exist_ok=True)
295+
missing_tools = set(REQUIRED_TOOLS) - set(config.managed_tools.keys())
296+
if len(missing_tools) == 0:
296297
click.echo(
297-
f"Tool {tool} not installed yet. "
298-
+ f"Use `commodore tool install {tool}` to install tools."
298+
"All required tools are already managed by Commodore.\n\n"
299+
+ "Use `commodore tool upgrade --all` to upgrade all tools to their latest versions."
299300
)
300301
return
301302

303+
for tool in sorted(REQUIRED_TOOLS):
304+
if tool in config.managed_tools:
305+
click.secho(f"Tool {tool} already managed, skipping...", fg="yellow")
306+
continue
307+
click.secho(f"Installing tool {tool}", bold=True)
308+
do_install(config, tool, None)
309+
310+
311+
def do_upgrade(config: Config, tool: str, version: Optional[str]):
302312
# The kustomize install script is unhappy if the kustomize binary is already
303313
# present in the install directory. To keep it simple, we unlink the old
304314
# tool binary for all tools when doing an upgrade (note that this generates
@@ -312,3 +322,31 @@ def upgrade_tool(config: Config, tool: str, version: Optional[str]):
312322
if tool_path.parent == MANAGED_TOOLS_PATH:
313323
tool_path.unlink()
314324
do_install(config, tool, version)
325+
326+
327+
def upgrade_tool(config: Config, tool: str, version: Optional[str]):
328+
check_known(tool)
329+
if tool not in config.managed_tools:
330+
click.echo(
331+
f"Tool {tool} not installed yet. "
332+
+ f"Use `commodore tool install {tool}` to install tools."
333+
)
334+
return
335+
336+
do_upgrade(config, tool, version)
337+
338+
339+
def upgrade_all_tools(config: Config):
340+
if len(config.managed_tools) == 0:
341+
click.echo(
342+
"No tools managed by Commodore yet.\n\n"
343+
+ "Use `commodore tool install --missing` to install the latest version for all required tools."
344+
)
345+
return
346+
347+
for tool in sorted(REQUIRED_TOOLS):
348+
if tool not in config.managed_tools:
349+
click.secho(f"Tool {tool} not managed, skipping...", fg="yellow")
350+
continue
351+
click.secho(f"Upgrading tool {tool}", bold=True)
352+
do_upgrade(config, tool, None)

docs/modules/ROOT/pages/reference/cli.adoc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,9 @@ However, labels added by previous runs can't be removed since we've got no easy
675675
Specify a version to install for the requested tool.
676676
By default, the latest version is installed.
677677

678+
*--missing*::
679+
Install the latest version for all currently unmanaged tools.
680+
This flag and providing a tool name on the command line are mutually exclusive.
678681

679682
== Tool Upgrade
680683

@@ -686,6 +689,10 @@ However, labels added by previous runs can't be removed since we've got no easy
686689
Specify a custom version to upgrade (or downgrade) to for the requested tool.
687690
By default, tools are upgraded to the latest version.
688691

692+
*--all*::
693+
Upgrade all currently managed tools to their latest versions.
694+
This flag and providing a tool name on the command line are mutually exclusive.
695+
689696
== Version
690697

691698
Show extended version information for Commodore.

docs/modules/ROOT/pages/reference/commands.adoc

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,12 +304,16 @@ For `jb`, the command directly downloads the requested version from the GitHub r
304304
Optionally, the command accepts a tool version to install.
305305
The command accepts versions prefixed with "v" and unprefixed versions.
306306

307+
commodore tool install --missing
308+
309+
Install the latest version of each required tool which isn't managed by Commodore yet.
310+
This variant of the command doesn't accept a tool name or the `--version` flag.
311+
307312
== Tool Upgrade
308313

309314
commodore tool upgrade TOOL
310315

311316
Upgrade (or downgrade) one of the required tools in `$XDG_CACHE_DIR/commodore/tools`.
312-
313317
The command will fail for tools which aren't managed by Commodore yet.
314318

315319
By default, the command will upgrade the tool to the latest available version.
@@ -319,6 +323,11 @@ For `jb`, the command directly downloads the requested version from the GitHub r
319323
Optionally, the command accepts a tool version to upgrade (or downgrade) to.
320324
The command accepts versions prefixed with "v" and unprefixed versions.
321325

326+
commodore tool upgrade --all
327+
328+
Upgrade all tools which are managed by Commodore to their latest versions.
329+
This variant of the command doesn't accept a tool name or the `--version` flag.
330+
322331
== Version
323332

324333
commodore version

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
@@ -467,6 +467,55 @@ def test_install_kustomize(config: Config, capfd, tmp_path, github_token):
467467
assert outlines[4] == f"GITHUB_TOKEN={github_token}"
468468

469469

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

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

0 commit comments

Comments
 (0)