clan-cli: machines delete: delete the machine's vars and secrets

When a machine is deleted with `clan machines delete`, remove its
vars and legacy secrets, and update any secrets that reference the
machine's key.

This command is a superset of `clan secrets machine delete`, and I am
wondering if we could remove the `clan secrets machine` subcommand,
unless there is an use case for having a machine defined without its
key, and any secrets/vars?

Note:

- This deletes the `ListSecretsOptions` dataclass, as it did not seem to
  bring any value, especially since `list_secrets` was receiving its
  individual members instead of the whole dataclass. We can always bring
  it back if complexity grows to demand it.
This commit is contained in:
Louis Opter
2025-02-07 11:31:38 +00:00
committed by Mic92
parent f7bec766bc
commit cd248b69db
3 changed files with 45 additions and 25 deletions

View File

@@ -208,16 +208,11 @@ def complete_secrets(
Provides completion functionality for clan secrets Provides completion functionality for clan secrets
""" """
from .clan_uri import Flake from .clan_uri import Flake
from .secrets.secrets import ListSecretsOptions, list_secrets from .secrets.secrets import list_secrets
flake = clan_dir_result if (clan_dir_result := clan_dir(None)) is not None else "." flake = clan_dir_result if (clan_dir_result := clan_dir(None)) is not None else "."
options = ListSecretsOptions( secrets = list_secrets(Flake(flake).path)
flake=Flake(flake),
pattern=None,
)
secrets = list_secrets(options.flake.path, options.pattern)
secrets_dict = dict.fromkeys(secrets, "secret") secrets_dict = dict.fromkeys(secrets, "secret")
return secrets_dict return secrets_dict

View File

@@ -1,25 +1,57 @@
import argparse import argparse
import logging import logging
import shutil import shutil
from pathlib import Path
from clan_cli import inventory
from clan_cli.api import API from clan_cli.api import API
from clan_cli.clan_uri import Flake from clan_cli.clan_uri import Flake
from clan_cli.completions import add_dynamic_completer, complete_machines from clan_cli.completions import add_dynamic_completer, complete_machines
from clan_cli.dirs import specific_machine_dir from clan_cli.dirs import specific_machine_dir
from clan_cli.inventory import delete from clan_cli.secrets.folders import sops_secrets_folder
from clan_cli.secrets.machines import has_machine as secrets_has_machine
from clan_cli.secrets.machines import remove_machine as secrets_machine_remove
from clan_cli.secrets.secrets import (
list_secrets,
)
from clan_cli.vars.list import (
public_store as vars_public_store,
)
from clan_cli.vars.list import (
secret_store as vars_secret_store,
)
from .machines import Machine
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@API.register @API.register
def delete_machine(flake: Flake, name: str) -> None: def delete_machine(flake: Flake, name: str) -> None:
delete(str(flake.path), {f"machines.{name}"}) inventory.delete(str(flake.path), {f"machines.{name}"})
changed_paths: list[Path] = []
# Remove the machine directory
folder = specific_machine_dir(flake.path, name) folder = specific_machine_dir(flake.path, name)
if folder.exists(): if folder.exists():
changed_paths.append(folder)
shutil.rmtree(folder) shutil.rmtree(folder)
# louis@(2025-02-04): clean-up legacy (pre-vars) secrets:
sops_folder = sops_secrets_folder(flake.path)
filter_fn = lambda secret_name: secret_name.startswith(f"{name}-")
for secret_name in list_secrets(flake.path, filter_fn):
secret_path = sops_folder / secret_name
changed_paths.append(secret_path)
shutil.rmtree(secret_path)
machine = Machine(name, flake)
changed_paths.extend(vars_public_store(machine).delete_store())
changed_paths.extend(vars_secret_store(machine).delete_store())
# Remove the machine's key, and update secrets & vars that referenced it:
if secrets_has_machine(flake.path, name):
secrets_machine_remove(flake.path, name)
def delete_command(args: argparse.Namespace) -> None: def delete_command(args: argparse.Namespace) -> None:
delete_machine(args.flake, args.name) delete_machine(args.flake, args.name)

View File

@@ -6,7 +6,6 @@ import os
import shutil import shutil
import sys import sys
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import IO from typing import IO
@@ -18,7 +17,6 @@ from clan_cli.completions import (
complete_users, complete_users,
) )
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.flake import Flake
from clan_cli.git import commit_files from clan_cli.git import commit_files
from . import sops from . import sops
@@ -325,31 +323,26 @@ def has_secret(secret_path: Path) -> bool:
return (secret_path / "secret").exists() return (secret_path / "secret").exists()
def list_secrets(flake_dir: Path, pattern: str | None = None) -> list[str]: def list_secrets(
flake_dir: Path, filter_fn: Callable[[str], bool] | None = None
) -> list[str]:
path = sops_secrets_folder(flake_dir) path = sops_secrets_folder(flake_dir)
def validate(name: str) -> bool: def validate(name: str) -> bool:
return ( return (
VALID_SECRET_NAME.match(name) is not None VALID_SECRET_NAME.match(name) is not None
and has_secret(sops_secrets_folder(flake_dir) / name) and has_secret(sops_secrets_folder(flake_dir) / name)
and (pattern is None or pattern in name) and (filter_fn is None or filter_fn(name) is True)
) )
return list_objects(path, validate) return list_objects(path, validate)
@dataclass
class ListSecretsOptions:
flake: Flake
pattern: str | None
def list_command(args: argparse.Namespace) -> None: def list_command(args: argparse.Namespace) -> None:
options = ListSecretsOptions( def filter_fn(name: str) -> bool:
flake=args.flake, return args.pattern in name
pattern=args.pattern,
) lst = list_secrets(args.flake.path, filter_fn if args.pattern else None)
lst = list_secrets(options.flake.path, options.pattern)
if len(lst) > 0: if len(lst) > 0:
print("\n".join(lst)) print("\n".join(lst))