From fc0e8fa1bd8efe40ccc653dddb90270b01d0d018 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Wed, 18 Jun 2025 13:41:04 +0200 Subject: [PATCH] pkgs/clan: Add `--tags` support to `clan machines update` --- pkgs/clan-cli/clan_cli/machines/cli.py | 10 ++++++ pkgs/clan-cli/clan_cli/machines/update.py | 35 +++++++++++++++---- .../clan_cli/tests/test_machines_cli.py | 25 +++++++++++++ 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/machines/cli.py b/pkgs/clan-cli/clan_cli/machines/cli.py index 9ddb53fdf..cf8935d0c 100644 --- a/pkgs/clan-cli/clan_cli/machines/cli.py +++ b/pkgs/clan-cli/clan_cli/machines/cli.py @@ -37,6 +37,16 @@ Examples: To exclude machines being updated `clan.deployment.requireExplicitUpdate = true;` can be set in the machine config. + $ clan machines update --tags [TAGS..] + Will update all machines that have the specified tags associated through the inventory. + If multiple tags are specified machines are matched against both tags. + + $ clan machines update --tags vm + Will update all machines that are associated with the "vm" tag through the inventory. + + $ clan machines update machine1 machine2 --tags production + Will update only machine1 and machine2 if they both have the "production" tag. + For more detailed information, visit: https://docs.clan.lol/guides/getting-started/deploy """ ), diff --git a/pkgs/clan-cli/clan_cli/machines/update.py b/pkgs/clan-cli/clan_cli/machines/update.py index 9965fb77f..9a925d36e 100644 --- a/pkgs/clan-cli/clan_cli/machines/update.py +++ b/pkgs/clan-cli/clan_cli/machines/update.py @@ -19,10 +19,11 @@ from clan_lib.ssh.remote import HostKeyCheck, Remote from clan_cli.completions import ( add_dynamic_completer, complete_machines, + complete_tags, ) from clan_cli.facts.generate import generate_facts from clan_cli.facts.upload import upload_secrets -from clan_cli.machines.list import list_full_machines +from clan_cli.machines.list import list_full_machines, query_machines_by_tags from clan_cli.vars.generate import generate_vars from clan_cli.vars.upload import upload_secret_vars @@ -213,10 +214,24 @@ def update_command(args: argparse.Namespace) -> None: raise ClanError(msg) machines: list[Machine] = [] - # if no machines are passed, we will update all machines - selected_machines = ( - args.machines if args.machines else list_full_machines(args.flake).keys() - ) + if args.tags: + tag_filtered_machines = query_machines_by_tags(args.flake, args.tags) + if args.machines: + selected_machines = [ + name for name in args.machines if name in tag_filtered_machines + ] + else: + selected_machines = list(tag_filtered_machines.keys()) + else: + selected_machines = ( + args.machines + if args.machines + else list(list_full_machines(args.flake).keys()) + ) + + if args.tags and not selected_machines: + msg = f"No machines found with tags: {', '.join(args.tags)}" + raise ClanError(msg) for machine_name in selected_machines: machine = Machine( @@ -244,7 +259,7 @@ def update_command(args: argparse.Namespace) -> None: return True machines_to_update = machines - implicit_all: bool = len(args.machines) == 0 + implicit_all: bool = len(args.machines) == 0 and not args.tags if implicit_all: machines_to_update = list(filter(filter_machine, machines)) @@ -314,6 +329,14 @@ def register_update_parser(parser: argparse.ArgumentParser) -> None: ) add_dynamic_completer(machines_parser, complete_machines) + tag_parser = parser.add_argument( + "--tags", + nargs="+", + default=[], + help="Tags that machines should be queried for. Multiple tags will intersect.", + ) + add_dynamic_completer(tag_parser, complete_tags) + parser.add_argument( "--host-key-check", choices=["strict", "ask", "tofu", "none"], diff --git a/pkgs/clan-cli/clan_cli/tests/test_machines_cli.py b/pkgs/clan-cli/clan_cli/tests/test_machines_cli.py index e3f326a99..9214c836c 100644 --- a/pkgs/clan-cli/clan_cli/tests/test_machines_cli.py +++ b/pkgs/clan-cli/clan_cli/tests/test_machines_cli.py @@ -65,6 +65,31 @@ def test_machine_subcommands( assert "vm2" in output.out +@pytest.mark.impure +def test_machines_update_with_tags( + test_flake_with_core: fixtures_flakes.FlakeForTest, + capture_output: CaptureOutput, +) -> None: + import argparse + + from clan_cli.machines.update import register_update_parser + + parser = argparse.ArgumentParser() + register_update_parser(parser) + + args = parser.parse_args(["--tags", "vm", "production"]) + assert hasattr(args, "tags") + assert args.tags == ["vm", "production"] + + args = parser.parse_args(["machine1", "machine2"]) + assert hasattr(args, "tags") + assert args.tags == [] + + args = parser.parse_args(["machine1", "--tags", "vm"]) + assert args.machines == ["machine1"] + assert args.tags == ["vm"] + + @pytest.mark.with_core def test_machine_delete( monkeypatch: pytest.MonkeyPatch,