From c5b16124efa4628abe5995fb11eca3372ec9e8a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Thu, 24 Aug 2023 16:58:22 +0200 Subject: [PATCH 1/6] add machine subcommand --- pkgs/clan-cli/clan_cli/machines/__init__.py | 16 +++++++++++-- pkgs/clan-cli/clan_cli/machines/create.py | 13 +++++++++++ pkgs/clan-cli/clan_cli/machines/delete.py | 17 ++++++++++++++ pkgs/clan-cli/clan_cli/machines/folders.py | 11 +++++++++ pkgs/clan-cli/clan_cli/machines/list.py | 25 +++++++++++++++++++++ pkgs/clan-cli/clan_cli/machines/types.py | 22 ++++++++++++++++++ pkgs/clan-cli/clan_cli/secrets/groups.py | 3 +-- pkgs/clan-cli/clan_cli/secrets/machines.py | 3 +-- pkgs/clan-cli/clan_cli/secrets/types.py | 19 ---------------- 9 files changed, 104 insertions(+), 25 deletions(-) create mode 100644 pkgs/clan-cli/clan_cli/machines/create.py create mode 100644 pkgs/clan-cli/clan_cli/machines/delete.py create mode 100644 pkgs/clan-cli/clan_cli/machines/folders.py create mode 100644 pkgs/clan-cli/clan_cli/machines/list.py create mode 100644 pkgs/clan-cli/clan_cli/machines/types.py diff --git a/pkgs/clan-cli/clan_cli/machines/__init__.py b/pkgs/clan-cli/clan_cli/machines/__init__.py index ed51575..fb12327 100644 --- a/pkgs/clan-cli/clan_cli/machines/__init__.py +++ b/pkgs/clan-cli/clan_cli/machines/__init__.py @@ -1,6 +1,9 @@ # !/usr/bin/env python3 import argparse +from .create import register_create_parser +from .delete import register_delete_parser +from .list import register_list_parser from .update import register_update_parser @@ -13,5 +16,14 @@ def register_parser(parser: argparse.ArgumentParser) -> None: required=True, ) - groups_parser = subparser.add_parser("update", help="Update a machine") - register_update_parser(groups_parser) + update_parser = subparser.add_parser("update", help="Update a machine") + register_update_parser(update_parser) + + create_parser = subparser.add_parser("create", help="Create a machine") + register_create_parser(create_parser) + + delete_parser = subparser.add_parser("delete", help="Delete a machine") + register_delete_parser(delete_parser) + + list_parser = subparser.add_parser("list", help="List machines") + register_list_parser(list_parser) diff --git a/pkgs/clan-cli/clan_cli/machines/create.py b/pkgs/clan-cli/clan_cli/machines/create.py new file mode 100644 index 0000000..7e59a78 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/machines/create.py @@ -0,0 +1,13 @@ +import argparse + +from .folders import machine_folder + + +def create_command(args: argparse.Namespace) -> None: + folder = machine_folder(args.host) + folder.mkdir(parents=True, exist_ok=True) + + +def register_create_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument("host", type=str) + parser.set_defaults(func=create_command) diff --git a/pkgs/clan-cli/clan_cli/machines/delete.py b/pkgs/clan-cli/clan_cli/machines/delete.py new file mode 100644 index 0000000..20dc3c0 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/machines/delete.py @@ -0,0 +1,17 @@ +import argparse + +from ..errors import ClanError +from .folders import machine_folder + + +def delete_command(args: argparse.Namespace) -> None: + folder = machine_folder(args.host) + if folder.exists(): + folder.rmdir() + else: + raise ClanError(f"Machine {args.host} does not exist") + + +def register_delete_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument("host", type=str) + parser.set_defaults(func=delete_command) diff --git a/pkgs/clan-cli/clan_cli/machines/folders.py b/pkgs/clan-cli/clan_cli/machines/folders.py new file mode 100644 index 0000000..b7a5af2 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/machines/folders.py @@ -0,0 +1,11 @@ +from pathlib import Path + +from ..dirs import get_clan_flake_toplevel + + +def machines_folder() -> Path: + return get_clan_flake_toplevel() / "machines" + + +def machine_folder(machine: str) -> Path: + return machines_folder() / machine diff --git a/pkgs/clan-cli/clan_cli/machines/list.py b/pkgs/clan-cli/clan_cli/machines/list.py new file mode 100644 index 0000000..dc4755f --- /dev/null +++ b/pkgs/clan-cli/clan_cli/machines/list.py @@ -0,0 +1,25 @@ +import argparse +import os + +from .folders import machines_folder +from .types import validate_hostname + + +def list_machines() -> list[str]: + path = machines_folder() + if not path.exists(): + return [] + objs: list[str] = [] + for f in os.listdir(path): + if validate_hostname(f): + objs.append(f) + return objs + + +def list_command(args: argparse.Namespace) -> None: + for machine in list_machines(): + print(machine) + + +def register_list_parser(parser: argparse.ArgumentParser) -> None: + parser.set_defaults(func=list_command) diff --git a/pkgs/clan-cli/clan_cli/machines/types.py b/pkgs/clan-cli/clan_cli/machines/types.py new file mode 100644 index 0000000..5c9ce48 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/machines/types.py @@ -0,0 +1,22 @@ +import argparse +import re + +VALID_HOSTNAME = re.compile(r"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", re.IGNORECASE) + + +def validate_hostname(hostname: str) -> bool: + if len(hostname) > 63: + return False + return VALID_HOSTNAME.match(hostname) is not None + + +def machine_name_type(arg_value: str) -> str: + if len(arg_value) > 63: + raise argparse.ArgumentTypeError( + "Machine name must be less than 63 characters long" + ) + if not VALID_HOSTNAME.match(arg_value): + raise argparse.ArgumentTypeError( + "Invalid character in machine name. Allowed characters are a-z, 0-9, ., -, and _. Must not start with a number" + ) + return arg_value diff --git a/pkgs/clan-cli/clan_cli/secrets/groups.py b/pkgs/clan-cli/clan_cli/secrets/groups.py index 2d13614..37b3cb2 100644 --- a/pkgs/clan-cli/clan_cli/secrets/groups.py +++ b/pkgs/clan-cli/clan_cli/secrets/groups.py @@ -3,15 +3,14 @@ import os from pathlib import Path from ..errors import ClanError +from ..machines.types import machine_name_type, validate_hostname from . import secrets from .folders import sops_groups_folder, sops_machines_folder, sops_users_folder from .types import ( VALID_USER_NAME, group_name_type, - machine_name_type, secret_name_type, user_name_type, - validate_hostname, ) diff --git a/pkgs/clan-cli/clan_cli/secrets/machines.py b/pkgs/clan-cli/clan_cli/secrets/machines.py index ebb9fb8..80a07e6 100644 --- a/pkgs/clan-cli/clan_cli/secrets/machines.py +++ b/pkgs/clan-cli/clan_cli/secrets/machines.py @@ -1,13 +1,12 @@ import argparse +from ..machines.types import machine_name_type, validate_hostname from . import secrets from .folders import list_objects, remove_object, sops_machines_folder from .sops import write_key from .types import ( - machine_name_type, public_or_private_age_key_type, secret_name_type, - validate_hostname, ) diff --git a/pkgs/clan-cli/clan_cli/secrets/types.py b/pkgs/clan-cli/clan_cli/secrets/types.py index 0005376..874f8ad 100644 --- a/pkgs/clan-cli/clan_cli/secrets/types.py +++ b/pkgs/clan-cli/clan_cli/secrets/types.py @@ -9,13 +9,6 @@ from .sops import get_public_key VALID_SECRET_NAME = re.compile(r"^[a-zA-Z0-9._-]+$") VALID_USER_NAME = re.compile(r"^[a-z_]([a-z0-9_-]{0,31})?$") -VALID_HOSTNAME = re.compile(r"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", re.IGNORECASE) - - -def validate_hostname(hostname: str) -> bool: - if len(hostname) > 63: - return False - return VALID_HOSTNAME.match(hostname) is not None def secret_name_type(arg_value: str) -> str: @@ -26,18 +19,6 @@ def secret_name_type(arg_value: str) -> str: return arg_value -def machine_name_type(arg_value: str) -> str: - if len(arg_value) > 63: - raise argparse.ArgumentTypeError( - "Machine name must be less than 63 characters long" - ) - if not VALID_SECRET_NAME.match(arg_value): - raise argparse.ArgumentTypeError( - "Invalid character in machine name. Allowed characters are a-z, 0-9, ., -, and _. Must not start with a number" - ) - return arg_value - - def public_or_private_age_key_type(arg_value: str) -> str: if os.path.isfile(arg_value): arg_value = Path(arg_value).read_text().strip() From 3b099ccb89b0358ff9377b988a3f88bf7b5cdfc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Thu, 24 Aug 2023 17:09:04 +0200 Subject: [PATCH 2/6] clan-cli: move arg parsing to extra method --- pkgs/clan-cli/clan_cli/__init__.py | 22 +++++++++++++--------- pkgs/clan-cli/tests/test_cli.py | 20 ++++---------------- 2 files changed, 17 insertions(+), 25 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/__init__.py b/pkgs/clan-cli/clan_cli/__init__.py index fa8573b..d12d0c6 100644 --- a/pkgs/clan-cli/clan_cli/__init__.py +++ b/pkgs/clan-cli/clan_cli/__init__.py @@ -16,8 +16,7 @@ except ImportError: pass -# this will be the entrypoint under /bin/clan (see pyproject.toml config) -def main() -> None: +def parse_args(args: list[str]) -> argparse.Namespace: parser = argparse.ArgumentParser(description="cLAN tool") subparsers = parser.add_subparsers() @@ -48,13 +47,18 @@ def main() -> None: if len(sys.argv) == 1: parser.print_help() - args = parser.parse_args() - if hasattr(args, "func"): - try: - args.func(args) - except ClanError as e: - print(f"{sys.argv[0]}: {e}") - sys.exit(1) + return parser.parse_args(args) + + +# this will be the entrypoint under /bin/clan (see pyproject.toml config) +def main() -> None: + args = parse_args(sys.argv[1:]) + assert hasattr(args, "func") + try: + args.func(args) + except ClanError as e: + print(f"{sys.argv[0]}: {e}") + sys.exit(1) if __name__ == "__main__": diff --git a/pkgs/clan-cli/tests/test_cli.py b/pkgs/clan-cli/tests/test_cli.py index 2028602..24cbd54 100644 --- a/pkgs/clan-cli/tests/test_cli.py +++ b/pkgs/clan-cli/tests/test_cli.py @@ -1,22 +1,10 @@ -import sys - import pytest - -import clan_cli +from cli import Cli -def test_no_args( - capsys: pytest.CaptureFixture, monkeypatch: pytest.MonkeyPatch -) -> None: - monkeypatch.setattr(sys, "argv", [""]) - clan_cli.main() - captured = capsys.readouterr() - assert captured.out.startswith("usage:") - - -def test_help(capsys: pytest.CaptureFixture, monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(sys, "argv", ["", "--help"]) +def test_help(capsys: pytest.CaptureFixture) -> None: + cli = Cli() with pytest.raises(SystemExit): - clan_cli.main() + cli.run(["--help"]) captured = capsys.readouterr() assert captured.out.startswith("usage:") From d600a927eab0fac566e4027638b242dcad3eff0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Thu, 24 Aug 2023 17:29:31 +0200 Subject: [PATCH 3/6] tests: generalize secret cli parser to work for all cli commands --- pkgs/clan-cli/clan_cli/__init__.py | 13 +-- .../tests/helpers/{secret_cli.py => cli.py} | 10 +- pkgs/clan-cli/tests/test_admin_cli.py | 13 +-- pkgs/clan-cli/tests/test_import_sops_cli.py | 19 ++-- pkgs/clan-cli/tests/test_secrets_cli.py | 91 ++++++++++--------- 5 files changed, 71 insertions(+), 75 deletions(-) rename pkgs/clan-cli/tests/helpers/{secret_cli.py => cli.py} (50%) diff --git a/pkgs/clan-cli/clan_cli/__init__.py b/pkgs/clan-cli/clan_cli/__init__.py index d12d0c6..9f49bb6 100644 --- a/pkgs/clan-cli/clan_cli/__init__.py +++ b/pkgs/clan-cli/clan_cli/__init__.py @@ -16,8 +16,8 @@ except ImportError: pass -def parse_args(args: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser(description="cLAN tool") +def create_parser(prog: Optional[str] = None) -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(prog=prog, description="cLAN tool") subparsers = parser.add_subparsers() parser_admin = subparsers.add_parser("admin", help="administrate a clan") @@ -46,14 +46,15 @@ def parse_args(args: list[str]) -> argparse.Namespace: if len(sys.argv) == 1: parser.print_help() - - return parser.parse_args(args) + return parser # this will be the entrypoint under /bin/clan (see pyproject.toml config) def main() -> None: - args = parse_args(sys.argv[1:]) - assert hasattr(args, "func") + parser = create_parser() + args = parser.parse_args() + if not hasattr(args, "func"): + return try: args.func(args) except ClanError as e: diff --git a/pkgs/clan-cli/tests/helpers/secret_cli.py b/pkgs/clan-cli/tests/helpers/cli.py similarity index 50% rename from pkgs/clan-cli/tests/helpers/secret_cli.py rename to pkgs/clan-cli/tests/helpers/cli.py index d43408d..ea633c2 100644 --- a/pkgs/clan-cli/tests/helpers/secret_cli.py +++ b/pkgs/clan-cli/tests/helpers/cli.py @@ -1,14 +1,14 @@ import argparse -from clan_cli.secrets import register_parser +from clan_cli import create_parser -class SecretCli: +class Cli: def __init__(self) -> None: - self.parser = argparse.ArgumentParser() - register_parser(self.parser) + self.parser = create_parser(prog="clan") def run(self, args: list[str]) -> argparse.Namespace: parsed = self.parser.parse_args(args) - parsed.func(parsed) + if hasattr(parsed, "func"): + parsed.func(parsed) return parsed diff --git a/pkgs/clan-cli/tests/test_admin_cli.py b/pkgs/clan-cli/tests/test_admin_cli.py index db829c8..dba43d1 100644 --- a/pkgs/clan-cli/tests/test_admin_cli.py +++ b/pkgs/clan-cli/tests/test_admin_cli.py @@ -1,21 +1,14 @@ -import argparse from typing import Union import pytest_subprocess.fake_process +from cli import Cli from pytest_subprocess import utils -from clan_cli import admin - - -def test_make_parser() -> None: - parser = argparse.ArgumentParser() - admin.register_parser(parser) - # using fp fixture from pytest-subprocess def test_create(fp: pytest_subprocess.fake_process.FakeProcess) -> None: cmd: list[Union[str, utils.Any]] = ["nix", "flake", "init", "-t", fp.any()] fp.register(cmd) - args = argparse.Namespace(folder="./my-clan") - admin.create(args) + cli = Cli() + cli.run(["admin", "--folder", "./my-clan", "create"]) assert fp.call_count(cmd) == 1 diff --git a/pkgs/clan-cli/tests/test_import_sops_cli.py b/pkgs/clan-cli/tests/test_import_sops_cli.py index 73a6a13..8c9cebb 100644 --- a/pkgs/clan-cli/tests/test_import_sops_cli.py +++ b/pkgs/clan-cli/tests/test_import_sops_cli.py @@ -2,8 +2,8 @@ from pathlib import Path from typing import TYPE_CHECKING import pytest +from cli import Cli from environment import mock_env -from secret_cli import SecretCli if TYPE_CHECKING: from age_keys import KeyPair @@ -15,19 +15,20 @@ def test_import_sops( capsys: pytest.CaptureFixture, age_keys: list["KeyPair"], ) -> None: - cli = SecretCli() + cli = Cli() with mock_env(SOPS_AGE_KEY=age_keys[1].privkey): - cli.run(["machines", "add", "machine1", age_keys[0].pubkey]) - cli.run(["users", "add", "user1", age_keys[1].pubkey]) - cli.run(["users", "add", "user2", age_keys[2].pubkey]) - cli.run(["groups", "add-user", "group1", "user1"]) - cli.run(["groups", "add-user", "group1", "user2"]) + cli.run(["secrets", "machines", "add", "machine1", age_keys[0].pubkey]) + cli.run(["secrets", "users", "add", "user1", age_keys[1].pubkey]) + cli.run(["secrets", "users", "add", "user2", age_keys[2].pubkey]) + cli.run(["secrets", "groups", "add-user", "group1", "user1"]) + cli.run(["secrets", "groups", "add-user", "group1", "user2"]) # To edit: # SOPS_AGE_KEY=AGE-SECRET-KEY-1U5ENXZQAY62NC78Y2WC0SEGRRMAEEKH79EYY5TH4GPFWJKEAY0USZ6X7YQ sops --age age14tva0txcrl0zes05x7gkx56qd6wd9q3nwecjac74xxzz4l47r44sv3fz62 ./data/secrets.yaml cli.run( [ + "secrets", "import-sops", "--group", "group1", @@ -37,10 +38,10 @@ def test_import_sops( ] ) capsys.readouterr() - cli.run(["users", "list"]) + cli.run(["secrets", "users", "list"]) users = sorted(capsys.readouterr().out.rstrip().split()) assert users == ["user1", "user2"] capsys.readouterr() - cli.run(["get", "secret-key"]) + cli.run(["secrets", "get", "secret-key"]) assert capsys.readouterr().out == "secret-value" diff --git a/pkgs/clan-cli/tests/test_secrets_cli.py b/pkgs/clan-cli/tests/test_secrets_cli.py index 1d5ec3d..fd2bbef 100644 --- a/pkgs/clan-cli/tests/test_secrets_cli.py +++ b/pkgs/clan-cli/tests/test_secrets_cli.py @@ -3,8 +3,8 @@ from pathlib import Path from typing import TYPE_CHECKING import pytest +from cli import Cli from environment import mock_env -from secret_cli import SecretCli from clan_cli.errors import ClanError @@ -18,16 +18,17 @@ def _test_identities( capsys: pytest.CaptureFixture, age_keys: list["KeyPair"], ) -> None: - cli = SecretCli() + cli = Cli() sops_folder = clan_flake / "sops" - cli.run([what, "add", "foo", age_keys[0].pubkey]) + cli.run(["secrets", what, "add", "foo", age_keys[0].pubkey]) assert (sops_folder / what / "foo" / "key.json").exists() with pytest.raises(ClanError): - cli.run([what, "add", "foo", age_keys[0].pubkey]) + cli.run(["secrets", what, "add", "foo", age_keys[0].pubkey]) cli.run( [ + "secrets", what, "add", "-f", @@ -37,18 +38,18 @@ def _test_identities( ) capsys.readouterr() # empty the buffer - cli.run([what, "list"]) + cli.run(["secrets", what, "list"]) out = capsys.readouterr() # empty the buffer assert "foo" in out.out - cli.run([what, "remove", "foo"]) + cli.run(["secrets", what, "remove", "foo"]) assert not (sops_folder / what / "foo" / "key.json").exists() with pytest.raises(ClanError): # already removed - cli.run([what, "remove", "foo"]) + cli.run(["secrets", what, "remove", "foo"]) capsys.readouterr() - cli.run([what, "list"]) + cli.run(["secrets", what, "list"]) out = capsys.readouterr() assert "foo" not in out.out @@ -68,32 +69,32 @@ def test_machines( def test_groups( clan_flake: Path, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"] ) -> None: - cli = SecretCli() + cli = Cli() capsys.readouterr() # empty the buffer - cli.run(["groups", "list"]) + cli.run(["secrets", "groups", "list"]) assert capsys.readouterr().out == "" with pytest.raises(ClanError): # machine does not exist yet - cli.run(["groups", "add-machine", "group1", "machine1"]) + cli.run(["secrets", "groups", "add-machine", "group1", "machine1"]) with pytest.raises(ClanError): # user does not exist yet - cli.run(["groups", "add-user", "groupb1", "user1"]) - cli.run(["machines", "add", "machine1", age_keys[0].pubkey]) - cli.run(["groups", "add-machine", "group1", "machine1"]) + cli.run(["secrets", "groups", "add-user", "groupb1", "user1"]) + cli.run(["secrets", "machines", "add", "machine1", age_keys[0].pubkey]) + cli.run(["secrets", "groups", "add-machine", "group1", "machine1"]) # Should this fail? - cli.run(["groups", "add-machine", "group1", "machine1"]) + cli.run(["secrets", "groups", "add-machine", "group1", "machine1"]) - cli.run(["users", "add", "user1", age_keys[0].pubkey]) - cli.run(["groups", "add-user", "group1", "user1"]) + cli.run(["secrets", "users", "add", "user1", age_keys[0].pubkey]) + cli.run(["secrets", "groups", "add-user", "group1", "user1"]) capsys.readouterr() # empty the buffer - cli.run(["groups", "list"]) + cli.run(["secrets", "groups", "list"]) out = capsys.readouterr().out assert "user1" in out assert "machine1" in out - cli.run(["groups", "remove-user", "group1", "user1"]) - cli.run(["groups", "remove-machine", "group1", "machine1"]) + cli.run(["secrets", "groups", "remove-user", "group1", "user1"]) + cli.run(["secrets", "groups", "remove-machine", "group1", "machine1"]) groups = os.listdir(clan_flake / "sops" / "groups") assert len(groups) == 0 @@ -101,65 +102,65 @@ def test_groups( def test_secrets( clan_flake: Path, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"] ) -> None: - cli = SecretCli() + cli = Cli() capsys.readouterr() # empty the buffer - cli.run(["list"]) + cli.run(["secrets", "list"]) assert capsys.readouterr().out == "" with mock_env( SOPS_NIX_SECRET="foo", SOPS_AGE_KEY_FILE=str(clan_flake / ".." / "age.key") ): with pytest.raises(ClanError): # does not exist yet - cli.run(["get", "nonexisting"]) - cli.run(["set", "key"]) + cli.run(["secrets", "get", "nonexisting"]) + cli.run(["secrets", "set", "key"]) capsys.readouterr() - cli.run(["get", "key"]) + cli.run(["secrets", "get", "key"]) assert capsys.readouterr().out == "foo" capsys.readouterr() - cli.run(["users", "list"]) + cli.run(["secrets", "users", "list"]) users = capsys.readouterr().out.rstrip().split("\n") assert len(users) == 1, f"users: {users}" owner = users[0] capsys.readouterr() # empty the buffer - cli.run(["list"]) + cli.run(["secrets", "list"]) assert capsys.readouterr().out == "key\n" - cli.run(["machines", "add", "machine1", age_keys[0].pubkey]) - cli.run(["machines", "add-secret", "machine1", "key"]) + cli.run(["secrets", "machines", "add", "machine1", age_keys[0].pubkey]) + cli.run(["secrets", "machines", "add-secret", "machine1", "key"]) with mock_env(SOPS_AGE_KEY=age_keys[0].privkey, SOPS_AGE_KEY_FILE=""): capsys.readouterr() - cli.run(["get", "key"]) + cli.run(["secrets", "get", "key"]) assert capsys.readouterr().out == "foo" - cli.run(["machines", "remove-secret", "machine1", "key"]) + cli.run(["secrets", "machines", "remove-secret", "machine1", "key"]) - cli.run(["users", "add", "user1", age_keys[1].pubkey]) - cli.run(["users", "add-secret", "user1", "key"]) + cli.run(["secrets", "users", "add", "user1", age_keys[1].pubkey]) + cli.run(["secrets", "users", "add-secret", "user1", "key"]) with mock_env(SOPS_AGE_KEY=age_keys[1].privkey, SOPS_AGE_KEY_FILE=""): capsys.readouterr() - cli.run(["get", "key"]) + cli.run(["secrets", "get", "key"]) assert capsys.readouterr().out == "foo" - cli.run(["users", "remove-secret", "user1", "key"]) + cli.run(["secrets", "users", "remove-secret", "user1", "key"]) with pytest.raises(ClanError): # does not exist yet - cli.run(["groups", "add-secret", "admin-group", "key"]) - cli.run(["groups", "add-user", "admin-group", "user1"]) - cli.run(["groups", "add-user", "admin-group", owner]) - cli.run(["groups", "add-secret", "admin-group", "key"]) + cli.run(["secrets", "groups", "add-secret", "admin-group", "key"]) + cli.run(["secrets", "groups", "add-user", "admin-group", "user1"]) + cli.run(["secrets", "groups", "add-user", "admin-group", owner]) + cli.run(["secrets", "groups", "add-secret", "admin-group", "key"]) capsys.readouterr() # empty the buffer - cli.run(["set", "--group", "admin-group", "key2"]) + cli.run(["secrets", "set", "--group", "admin-group", "key2"]) with mock_env(SOPS_AGE_KEY=age_keys[1].privkey, SOPS_AGE_KEY_FILE=""): capsys.readouterr() - cli.run(["get", "key"]) + cli.run(["secrets", "get", "key"]) assert capsys.readouterr().out == "foo" - cli.run(["groups", "remove-secret", "admin-group", "key"]) + cli.run(["secrets", "groups", "remove-secret", "admin-group", "key"]) - cli.run(["remove", "key"]) - cli.run(["remove", "key2"]) + cli.run(["secrets", "remove", "key"]) + cli.run(["secrets", "remove", "key2"]) capsys.readouterr() # empty the buffer - cli.run(["list"]) + cli.run(["secrets", "list"]) assert capsys.readouterr().out == "" From dcbf1613fb49d4639fac5882ad3ff32bc4c83ee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Thu, 24 Aug 2023 17:35:57 +0200 Subject: [PATCH 4/6] delete test_cli test we already cover this in integration tests --- pkgs/clan-cli/tests/test_machines_update_cli.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/pkgs/clan-cli/tests/test_machines_update_cli.py b/pkgs/clan-cli/tests/test_machines_update_cli.py index b8fb676..6c23493 100644 --- a/pkgs/clan-cli/tests/test_machines_update_cli.py +++ b/pkgs/clan-cli/tests/test_machines_update_cli.py @@ -1,21 +1,12 @@ -import argparse import os import shutil from pathlib import Path from tempfile import TemporaryDirectory -import pytest from environment import mock_env from host_group import HostGroup -from clan_cli.machines.update import deploy_nixos, register_update_parser - - -def test_cli() -> None: - parser = argparse.ArgumentParser() - register_update_parser(parser) - with pytest.raises(SystemExit): - parser.parse_args(["--help"]) +from clan_cli.machines.update import deploy_nixos def test_update(clan_flake: Path, host_group: HostGroup) -> None: From 6adfadd8b9dd5354fd42fe469d490d7e9beee2fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Thu, 24 Aug 2023 17:53:33 +0200 Subject: [PATCH 5/6] re-enable config subcommand if CLAN_OPTIONS_FILE is set --- pkgs/clan-cli/clan_cli/__init__.py | 10 +++++----- pkgs/clan-cli/tests/test_config.py | 22 +++++++--------------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/__init__.py b/pkgs/clan-cli/clan_cli/__init__.py index 9f49bb6..45fad67 100644 --- a/pkgs/clan-cli/clan_cli/__init__.py +++ b/pkgs/clan-cli/clan_cli/__init__.py @@ -1,11 +1,10 @@ import argparse +import os import sys from types import ModuleType from typing import Optional -from . import admin, machines, secrets, webui - -# from . import admin, config, secrets, update, webui +from . import admin, config, machines, secrets, webui from .errors import ClanError from .ssh import cli as ssh_cli @@ -24,8 +23,9 @@ def create_parser(prog: Optional[str] = None) -> argparse.ArgumentParser: admin.register_parser(parser_admin) # DISABLED: this currently crashes if a flake does not define .#clanOptions - # parser_config = subparsers.add_parser("config", help="set nixos configuration") - # config.register_parser(parser_config) + if os.environ.get("CLAN_OPTIONS_FILE") is not None: + parser_config = subparsers.add_parser("config", help="set nixos configuration") + config.register_parser(parser_config) parser_ssh = subparsers.add_parser("ssh", help="ssh to a remote machine") ssh_cli.register_parser(parser_ssh) diff --git a/pkgs/clan-cli/tests/test_config.py b/pkgs/clan-cli/tests/test_config.py index b573077..e0185d2 100644 --- a/pkgs/clan-cli/tests/test_config.py +++ b/pkgs/clan-cli/tests/test_config.py @@ -1,11 +1,10 @@ -import argparse import json -import sys import tempfile from pathlib import Path from typing import Any import pytest +from cli import Cli from clan_cli import config from clan_cli.config import parsing @@ -15,7 +14,7 @@ example_options = f"{Path(config.__file__).parent}/jsonschema/options.json" # use pytest.parametrize @pytest.mark.parametrize( - "argv,expected", + "args,expected", [ (["name", "DavHau"], {"name": "DavHau"}), ( @@ -27,25 +26,18 @@ example_options = f"{Path(config.__file__).parent}/jsonschema/options.json" ], ) def test_set_some_option( - argv: list[str], + args: list[str], expected: dict[str, Any], monkeypatch: pytest.MonkeyPatch, ) -> None: - # monkeypatch sys.argv + monkeypatch.setenv("CLAN_OPTIONS_FILE", example_options) + # create temporary file for out_file with tempfile.NamedTemporaryFile() as out_file: with open(out_file.name, "w") as f: json.dump({}, f) - monkeypatch.setattr( - sys, "argv", ["", "--quiet", "--settings-file", out_file.name] + argv - ) - parser = argparse.ArgumentParser() - config._register_parser( - parser=parser, - options=json.loads(Path(example_options).read_text()), - ) - args = parser.parse_args() - args.func(args) + cli = Cli() + cli.run(["config", "--quiet", "--settings-file", out_file.name] + args) json_out = json.loads(open(out_file.name).read()) assert json_out == expected From 58adf91af8588faac3d60a9385a9567153407f92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Thu, 24 Aug 2023 18:06:56 +0200 Subject: [PATCH 6/6] test machine subcommand --- pkgs/clan-cli/clan_cli/machines/__init__.py | 4 ++-- pkgs/clan-cli/tests/test_machines_cli.py | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 pkgs/clan-cli/tests/test_machines_cli.py diff --git a/pkgs/clan-cli/clan_cli/machines/__init__.py b/pkgs/clan-cli/clan_cli/machines/__init__.py index fb12327..05d44eb 100644 --- a/pkgs/clan-cli/clan_cli/machines/__init__.py +++ b/pkgs/clan-cli/clan_cli/machines/__init__.py @@ -22,8 +22,8 @@ def register_parser(parser: argparse.ArgumentParser) -> None: create_parser = subparser.add_parser("create", help="Create a machine") register_create_parser(create_parser) - delete_parser = subparser.add_parser("delete", help="Delete a machine") - register_delete_parser(delete_parser) + remove_parser = subparser.add_parser("remove", help="Remove a machine") + register_delete_parser(remove_parser) list_parser = subparser.add_parser("list", help="List machines") register_list_parser(list_parser) diff --git a/pkgs/clan-cli/tests/test_machines_cli.py b/pkgs/clan-cli/tests/test_machines_cli.py new file mode 100644 index 0000000..a2e3fa8 --- /dev/null +++ b/pkgs/clan-cli/tests/test_machines_cli.py @@ -0,0 +1,21 @@ +from pathlib import Path + +import pytest +from cli import Cli + + +def test_machine_subcommands(clan_flake: Path, capsys: pytest.CaptureFixture) -> None: + cli = Cli() + cli.run(["machines", "create", "machine1"]) + + capsys.readouterr() + cli.run(["machines", "list"]) + out = capsys.readouterr() + assert "machine1\n" == out.out + + cli.run(["machines", "remove", "machine1"]) + + capsys.readouterr() + cli.run(["machines", "list"]) + out = capsys.readouterr() + assert "" == out.out