Skip to content

Commit 2a49321

Browse files
committed
Add tool install --missing and tool upgrade --all
1 parent 93191df commit 2a49321

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 install tool` 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 upgrade tool` 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
@@ -292,15 +292,25 @@ def install_tool(config: Config, tool: str, version: Optional[str]):
292292
do_install(config, tool, version)
293293

294294

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

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