secrets: ensure all added/deleted files get committed
This commit is contained in:
@@ -63,7 +63,9 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
nodes.client = {
|
nodes.client = {
|
||||||
environment.systemPackages = [ self.packages.${pkgs.system}.clan-cli ];
|
environment.systemPackages = [
|
||||||
|
self.packages.${pkgs.system}.clan-cli
|
||||||
|
] ++ self.packages.${pkgs.system}.clan-cli.runtimeDependencies;
|
||||||
environment.etc."install-closure".source = "${closureInfo}/store-paths";
|
environment.etc."install-closure".source = "${closureInfo}/store-paths";
|
||||||
virtualisation.memorySize = 2048;
|
virtualisation.memorySize = 2048;
|
||||||
nix.settings = {
|
nix.settings = {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class SecretStore(SecretStoreBase):
|
|||||||
# no need to generate keys if we don't manage secrets
|
# no need to generate keys if we don't manage secrets
|
||||||
if not hasattr(self.machine, "facts_data"):
|
if not hasattr(self.machine, "facts_data"):
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.machine.facts_data:
|
if not self.machine.facts_data:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ def commit_files(
|
|||||||
repo_dir: Path,
|
repo_dir: Path,
|
||||||
commit_message: str | None = None,
|
commit_message: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if not file_paths:
|
||||||
|
return
|
||||||
# check that the file is in the git repository
|
# check that the file is in the git repository
|
||||||
for file_path in file_paths:
|
for file_path in file_paths:
|
||||||
if not Path(file_path).resolve().is_relative_to(repo_dir.resolve()):
|
if not Path(file_path).resolve().is_relative_to(repo_dir.resolve()):
|
||||||
|
|||||||
@@ -33,10 +33,13 @@ def list_objects(path: Path, is_valid: Callable[[str], bool]) -> list[str]:
|
|||||||
return objs
|
return objs
|
||||||
|
|
||||||
|
|
||||||
def remove_object(path: Path, name: str) -> None:
|
def remove_object(path: Path, name: str) -> list[Path]:
|
||||||
|
paths_to_commit = []
|
||||||
try:
|
try:
|
||||||
shutil.rmtree(path / name)
|
shutil.rmtree(path / name)
|
||||||
|
paths_to_commit.append(path / name)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise ClanError(f"{name} not found in {path}")
|
raise ClanError(f"{name} not found in {path}")
|
||||||
if not os.listdir(path):
|
if not os.listdir(path):
|
||||||
os.rmdir(path)
|
os.rmdir(path)
|
||||||
|
return paths_to_commit
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import argparse
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from clan_cli.git import commit_files
|
||||||
|
|
||||||
from ..errors import ClanError
|
from ..errors import ClanError
|
||||||
from ..machines.types import machine_name_type, validate_hostname
|
from ..machines.types import machine_name_type, validate_hostname
|
||||||
from . import secrets
|
from . import secrets
|
||||||
@@ -87,19 +89,21 @@ def list_directory(directory: Path) -> str:
|
|||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
|
||||||
def update_group_keys(flake_dir: Path, group: str) -> None:
|
def update_group_keys(flake_dir: Path, group: str) -> list[Path]:
|
||||||
|
updated_paths = []
|
||||||
for secret_ in secrets.list_secrets(flake_dir):
|
for secret_ in secrets.list_secrets(flake_dir):
|
||||||
secret = sops_secrets_folder(flake_dir) / secret_
|
secret = sops_secrets_folder(flake_dir) / secret_
|
||||||
if (secret / "groups" / group).is_symlink():
|
if (secret / "groups" / group).is_symlink():
|
||||||
update_keys(
|
updated_paths += update_keys(
|
||||||
secret,
|
secret,
|
||||||
list(sorted(secrets.collect_keys_for_path(secret))),
|
list(sorted(secrets.collect_keys_for_path(secret))),
|
||||||
)
|
)
|
||||||
|
return updated_paths
|
||||||
|
|
||||||
|
|
||||||
def add_member(
|
def add_member(
|
||||||
flake_dir: Path, group_folder: Path, source_folder: Path, name: str
|
flake_dir: Path, group_folder: Path, source_folder: Path, name: str
|
||||||
) -> None:
|
) -> list[Path]:
|
||||||
source = source_folder / name
|
source = source_folder / name
|
||||||
if not source.exists():
|
if not source.exists():
|
||||||
msg = f"{name} does not exist in {source_folder}: "
|
msg = f"{name} does not exist in {source_folder}: "
|
||||||
@@ -114,7 +118,7 @@ def add_member(
|
|||||||
)
|
)
|
||||||
os.remove(user_target)
|
os.remove(user_target)
|
||||||
user_target.symlink_to(os.path.relpath(source, user_target.parent))
|
user_target.symlink_to(os.path.relpath(source, user_target.parent))
|
||||||
update_group_keys(flake_dir, group_folder.parent.name)
|
return update_group_keys(flake_dir, group_folder.parent.name)
|
||||||
|
|
||||||
|
|
||||||
def remove_member(flake_dir: Path, group_folder: Path, name: str) -> None:
|
def remove_member(flake_dir: Path, group_folder: Path, name: str) -> None:
|
||||||
@@ -136,9 +140,14 @@ def remove_member(flake_dir: Path, group_folder: Path, name: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def add_user(flake_dir: Path, group: str, name: str) -> None:
|
def add_user(flake_dir: Path, group: str, name: str) -> None:
|
||||||
add_member(
|
updated_files = add_member(
|
||||||
flake_dir, users_folder(flake_dir, group), sops_users_folder(flake_dir), name
|
flake_dir, users_folder(flake_dir, group), sops_users_folder(flake_dir), name
|
||||||
)
|
)
|
||||||
|
commit_files(
|
||||||
|
updated_files,
|
||||||
|
flake_dir,
|
||||||
|
f"Add user {name} to group {group}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def add_user_command(args: argparse.Namespace) -> None:
|
def add_user_command(args: argparse.Namespace) -> None:
|
||||||
@@ -154,12 +163,17 @@ def remove_user_command(args: argparse.Namespace) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def add_machine(flake_dir: Path, group: str, name: str) -> None:
|
def add_machine(flake_dir: Path, group: str, name: str) -> None:
|
||||||
add_member(
|
updated_files = add_member(
|
||||||
flake_dir,
|
flake_dir,
|
||||||
machines_folder(flake_dir, group),
|
machines_folder(flake_dir, group),
|
||||||
sops_machines_folder(flake_dir),
|
sops_machines_folder(flake_dir),
|
||||||
name,
|
name,
|
||||||
)
|
)
|
||||||
|
commit_files(
|
||||||
|
updated_files,
|
||||||
|
flake_dir,
|
||||||
|
f"Add machine {name} to group {group}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def add_machine_command(args: argparse.Namespace) -> None:
|
def add_machine_command(args: argparse.Namespace) -> None:
|
||||||
@@ -189,7 +203,14 @@ def add_secret_command(args: argparse.Namespace) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def remove_secret(flake_dir: Path, group: str, name: str) -> None:
|
def remove_secret(flake_dir: Path, group: str, name: str) -> None:
|
||||||
secrets.disallow_member(secrets.groups_folder(flake_dir, name), group)
|
updated_paths = secrets.disallow_member(
|
||||||
|
secrets.groups_folder(flake_dir, name), group
|
||||||
|
)
|
||||||
|
commit_files(
|
||||||
|
updated_paths,
|
||||||
|
flake_dir,
|
||||||
|
f"Remove group {group} from secret {name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def remove_secret_command(args: argparse.Namespace) -> None:
|
def remove_secret_command(args: argparse.Namespace) -> None:
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import argparse
|
import argparse
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from clan_cli.git import commit_files
|
||||||
|
|
||||||
from .. import tty
|
from .. import tty
|
||||||
from ..errors import ClanError
|
from ..errors import ClanError
|
||||||
from .secrets import update_secrets
|
from .secrets import update_secrets
|
||||||
@@ -11,9 +13,7 @@ def generate_key() -> str:
|
|||||||
path = default_sops_key_path()
|
path = default_sops_key_path()
|
||||||
if path.exists():
|
if path.exists():
|
||||||
raise ClanError(f"Key already exists at {path}")
|
raise ClanError(f"Key already exists at {path}")
|
||||||
priv_key, pub_key = generate_private_key()
|
priv_key, pub_key = generate_private_key(out_file=path)
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
path.write_text(priv_key)
|
|
||||||
return pub_key
|
return pub_key
|
||||||
|
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ def show_command(args: argparse.Namespace) -> None:
|
|||||||
|
|
||||||
def update_command(args: argparse.Namespace) -> None:
|
def update_command(args: argparse.Namespace) -> None:
|
||||||
flake_dir = Path(args.flake)
|
flake_dir = Path(args.flake)
|
||||||
update_secrets(flake_dir)
|
commit_files(update_secrets(flake_dir), flake_dir, "Updated secrets with new keys.")
|
||||||
|
|
||||||
|
|
||||||
def register_key_parser(parser: argparse.ArgumentParser) -> None:
|
def register_key_parser(parser: argparse.ArgumentParser) -> None:
|
||||||
|
|||||||
@@ -21,14 +21,19 @@ def add_machine(flake_dir: Path, name: str, key: str, force: bool) -> None:
|
|||||||
|
|
||||||
paths.extend(update_secrets(flake_dir, filter_secrets=filter_machine_secrets))
|
paths.extend(update_secrets(flake_dir, filter_secrets=filter_machine_secrets))
|
||||||
commit_files(
|
commit_files(
|
||||||
[path],
|
paths,
|
||||||
flake_dir,
|
flake_dir,
|
||||||
f"Add machine {name} to secrets",
|
f"Add machine {name} to secrets",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def remove_machine(flake_dir: Path, name: str) -> None:
|
def remove_machine(flake_dir: Path, name: str) -> None:
|
||||||
remove_object(sops_machines_folder(flake_dir), name)
|
removed_paths = remove_object(sops_machines_folder(flake_dir), name)
|
||||||
|
commit_files(
|
||||||
|
removed_paths,
|
||||||
|
flake_dir,
|
||||||
|
f"Remove machine {name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_machine(flake_dir: Path, name: str) -> str:
|
def get_machine(flake_dir: Path, name: str) -> str:
|
||||||
@@ -49,20 +54,27 @@ def list_machines(flake_dir: Path) -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
def add_secret(flake_dir: Path, machine: str, secret: str) -> None:
|
def add_secret(flake_dir: Path, machine: str, secret: str) -> None:
|
||||||
path = secrets.allow_member(
|
paths = secrets.allow_member(
|
||||||
secrets.machines_folder(flake_dir, secret),
|
secrets.machines_folder(flake_dir, secret),
|
||||||
sops_machines_folder(flake_dir),
|
sops_machines_folder(flake_dir),
|
||||||
machine,
|
machine,
|
||||||
)
|
)
|
||||||
commit_files(
|
commit_files(
|
||||||
[path],
|
paths,
|
||||||
flake_dir,
|
flake_dir,
|
||||||
f"Add {machine} to secret",
|
f"Add {machine} to secret",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def remove_secret(flake_dir: Path, machine: str, secret: str) -> None:
|
def remove_secret(flake_dir: Path, machine: str, secret: str) -> None:
|
||||||
secrets.disallow_member(secrets.machines_folder(flake_dir, secret), machine)
|
updated_paths = secrets.disallow_member(
|
||||||
|
secrets.machines_folder(flake_dir, secret), machine
|
||||||
|
)
|
||||||
|
commit_files(
|
||||||
|
updated_paths,
|
||||||
|
flake_dir,
|
||||||
|
f"Remove {machine} from secret {secret}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def list_command(args: argparse.Namespace) -> None:
|
def list_command(args: argparse.Namespace) -> None:
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ def encrypt_secret(
|
|||||||
|
|
||||||
files_to_commit = []
|
files_to_commit = []
|
||||||
for user in add_users:
|
for user in add_users:
|
||||||
files_to_commit.append(
|
files_to_commit.extend(
|
||||||
allow_member(
|
allow_member(
|
||||||
users_folder(flake_dir, secret.name),
|
users_folder(flake_dir, secret.name),
|
||||||
sops_users_folder(flake_dir),
|
sops_users_folder(flake_dir),
|
||||||
@@ -95,7 +95,7 @@ def encrypt_secret(
|
|||||||
)
|
)
|
||||||
|
|
||||||
for machine in add_machines:
|
for machine in add_machines:
|
||||||
files_to_commit.append(
|
files_to_commit.extend(
|
||||||
allow_member(
|
allow_member(
|
||||||
machines_folder(flake_dir, secret.name),
|
machines_folder(flake_dir, secret.name),
|
||||||
sops_machines_folder(flake_dir),
|
sops_machines_folder(flake_dir),
|
||||||
@@ -105,7 +105,7 @@ def encrypt_secret(
|
|||||||
)
|
)
|
||||||
|
|
||||||
for group in add_groups:
|
for group in add_groups:
|
||||||
files_to_commit.append(
|
files_to_commit.extend(
|
||||||
allow_member(
|
allow_member(
|
||||||
groups_folder(flake_dir, secret.name),
|
groups_folder(flake_dir, secret.name),
|
||||||
sops_groups_folder(flake_dir),
|
sops_groups_folder(flake_dir),
|
||||||
@@ -118,7 +118,7 @@ def encrypt_secret(
|
|||||||
|
|
||||||
if key.pubkey not in keys:
|
if key.pubkey not in keys:
|
||||||
keys.add(key.pubkey)
|
keys.add(key.pubkey)
|
||||||
files_to_commit.append(
|
files_to_commit.extend(
|
||||||
allow_member(
|
allow_member(
|
||||||
users_folder(flake_dir, secret.name),
|
users_folder(flake_dir, secret.name),
|
||||||
sops_users_folder(flake_dir),
|
sops_users_folder(flake_dir),
|
||||||
@@ -180,7 +180,7 @@ def list_directory(directory: Path) -> str:
|
|||||||
|
|
||||||
def allow_member(
|
def allow_member(
|
||||||
group_folder: Path, source_folder: Path, name: str, do_update_keys: bool = True
|
group_folder: Path, source_folder: Path, name: str, do_update_keys: bool = True
|
||||||
) -> Path:
|
) -> list[Path]:
|
||||||
source = source_folder / name
|
source = source_folder / name
|
||||||
if not source.exists():
|
if not source.exists():
|
||||||
msg = f"Cannot encrypt {group_folder.parent.name} for '{name}' group. '{name}' group does not exist in {source_folder}: "
|
msg = f"Cannot encrypt {group_folder.parent.name} for '{name}' group. '{name}' group does not exist in {source_folder}: "
|
||||||
@@ -196,15 +196,18 @@ def allow_member(
|
|||||||
os.remove(user_target)
|
os.remove(user_target)
|
||||||
|
|
||||||
user_target.symlink_to(os.path.relpath(source, user_target.parent))
|
user_target.symlink_to(os.path.relpath(source, user_target.parent))
|
||||||
|
changed = [user_target]
|
||||||
if do_update_keys:
|
if do_update_keys:
|
||||||
update_keys(
|
changed.extend(
|
||||||
group_folder.parent,
|
update_keys(
|
||||||
list(sorted(collect_keys_for_path(group_folder.parent))),
|
group_folder.parent,
|
||||||
|
list(sorted(collect_keys_for_path(group_folder.parent))),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return user_target
|
return changed
|
||||||
|
|
||||||
|
|
||||||
def disallow_member(group_folder: Path, name: str) -> None:
|
def disallow_member(group_folder: Path, name: str) -> list[Path]:
|
||||||
target = group_folder / name
|
target = group_folder / name
|
||||||
if not target.exists():
|
if not target.exists():
|
||||||
msg = f"{name} does not exist in group in {group_folder}: "
|
msg = f"{name} does not exist in group in {group_folder}: "
|
||||||
@@ -225,7 +228,7 @@ def disallow_member(group_folder: Path, name: str) -> None:
|
|||||||
if len(os.listdir(group_folder.parent)) == 0:
|
if len(os.listdir(group_folder.parent)) == 0:
|
||||||
os.rmdir(group_folder.parent)
|
os.rmdir(group_folder.parent)
|
||||||
|
|
||||||
update_keys(
|
return update_keys(
|
||||||
target.parent.parent, list(sorted(collect_keys_for_path(group_folder.parent)))
|
target.parent.parent, list(sorted(collect_keys_for_path(group_folder.parent)))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ def get_public_key(privkey: str) -> str:
|
|||||||
return res.stdout.strip()
|
return res.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
def generate_private_key() -> tuple[str, str]:
|
def generate_private_key(out_file: Path | None = None) -> tuple[str, str]:
|
||||||
cmd = nix_shell(["nixpkgs#age"], ["age-keygen"])
|
cmd = nix_shell(["nixpkgs#age"], ["age-keygen"])
|
||||||
try:
|
try:
|
||||||
proc = run(cmd)
|
proc = run(cmd)
|
||||||
@@ -50,6 +50,9 @@ def generate_private_key() -> tuple[str, str]:
|
|||||||
raise ClanError("Could not find public key in age-keygen output")
|
raise ClanError("Could not find public key in age-keygen output")
|
||||||
if not private_key:
|
if not private_key:
|
||||||
raise ClanError("Could not find private key in age-keygen output")
|
raise ClanError("Could not find private key in age-keygen output")
|
||||||
|
if out_file:
|
||||||
|
out_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
out_file.write_text(res)
|
||||||
return private_key, pubkey
|
return private_key, pubkey
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
raise ClanError("Failed to generate private sops key") from e
|
raise ClanError("Failed to generate private sops key") from e
|
||||||
|
|||||||
@@ -32,7 +32,12 @@ def add_user(flake_dir: Path, name: str, key: str, force: bool) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def remove_user(flake_dir: Path, name: str) -> None:
|
def remove_user(flake_dir: Path, name: str) -> None:
|
||||||
remove_object(sops_users_folder(flake_dir), name)
|
removed_paths = remove_object(sops_users_folder(flake_dir), name)
|
||||||
|
commit_files(
|
||||||
|
removed_paths,
|
||||||
|
flake_dir,
|
||||||
|
f"Remove user {name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_user(flake_dir: Path, name: str) -> str:
|
def get_user(flake_dir: Path, name: str) -> str:
|
||||||
@@ -52,13 +57,25 @@ def list_users(flake_dir: Path) -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
def add_secret(flake_dir: Path, user: str, secret: str) -> None:
|
def add_secret(flake_dir: Path, user: str, secret: str) -> None:
|
||||||
secrets.allow_member(
|
updated_paths = secrets.allow_member(
|
||||||
secrets.users_folder(flake_dir, secret), sops_users_folder(flake_dir), user
|
secrets.users_folder(flake_dir, secret), sops_users_folder(flake_dir), user
|
||||||
)
|
)
|
||||||
|
commit_files(
|
||||||
|
updated_paths,
|
||||||
|
flake_dir,
|
||||||
|
f"Add {user} to secret",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def remove_secret(flake_dir: Path, user: str, secret: str) -> None:
|
def remove_secret(flake_dir: Path, user: str, secret: str) -> None:
|
||||||
secrets.disallow_member(secrets.users_folder(flake_dir, secret), user)
|
updated_paths = secrets.disallow_member(
|
||||||
|
secrets.users_folder(flake_dir, secret), user
|
||||||
|
)
|
||||||
|
commit_files(
|
||||||
|
updated_paths,
|
||||||
|
flake_dir,
|
||||||
|
f"Remove {user} from secret",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def list_command(args: argparse.Namespace) -> None:
|
def list_command(args: argparse.Namespace) -> None:
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ python3.pkgs.buildPythonApplication {
|
|||||||
# Define and expose the tests and checks to run in CI
|
# Define and expose the tests and checks to run in CI
|
||||||
passthru.tests =
|
passthru.tests =
|
||||||
(lib.mapAttrs' (n: lib.nameValuePair "clan-dep-${n}") runtimeDependenciesAsSet)
|
(lib.mapAttrs' (n: lib.nameValuePair "clan-dep-${n}") runtimeDependenciesAsSet)
|
||||||
// rec {
|
// {
|
||||||
clan-pytest-without-core =
|
clan-pytest-without-core =
|
||||||
runCommand "clan-pytest-without-core"
|
runCommand "clan-pytest-without-core"
|
||||||
{ nativeBuildInputs = [ pythonWithTestDeps ] ++ testDependencies; }
|
{ nativeBuildInputs = [ pythonWithTestDeps ] ++ testDependencies; }
|
||||||
|
|||||||
@@ -175,6 +175,18 @@ def test_flake(
|
|||||||
monkeypatch: pytest.MonkeyPatch, temporary_home: Path
|
monkeypatch: pytest.MonkeyPatch, temporary_home: Path
|
||||||
) -> Iterator[FlakeForTest]:
|
) -> Iterator[FlakeForTest]:
|
||||||
yield from create_flake(monkeypatch, temporary_home, "test_flake")
|
yield from create_flake(monkeypatch, temporary_home, "test_flake")
|
||||||
|
# check that git diff on ./sops is empty
|
||||||
|
if (temporary_home / "test_flake" / "sops").exists():
|
||||||
|
git_proc = sp.run(
|
||||||
|
["git", "diff", "--exit-code", "./sops"],
|
||||||
|
cwd=temporary_home / "test_flake",
|
||||||
|
stderr=sp.PIPE,
|
||||||
|
)
|
||||||
|
if git_proc.returncode != 0:
|
||||||
|
log.error(git_proc.stderr.decode())
|
||||||
|
raise Exception(
|
||||||
|
"git diff on ./sops is not empty. This should not happen as all changes should be committed"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
Reference in New Issue
Block a user