Merge pull request 'vars/sops/shared: add machines key on demand' (#2086) from DavHau/clan-core:DavHau-dave into main
This commit is contained in:
@@ -22,19 +22,19 @@ from .sops import read_key, write_key
|
|||||||
from .types import public_or_private_age_key_type, secret_name_type
|
from .types import public_or_private_age_key_type, secret_name_type
|
||||||
|
|
||||||
|
|
||||||
def add_machine(flake_dir: Path, name: str, key: str, force: bool) -> None:
|
def add_machine(flake_dir: Path, machine: str, key: str, force: bool) -> None:
|
||||||
path = sops_machines_folder(flake_dir) / name
|
path = sops_machines_folder(flake_dir) / machine
|
||||||
write_key(path, key, force)
|
write_key(path, key, force)
|
||||||
paths = [path]
|
paths = [path]
|
||||||
|
|
||||||
def filter_machine_secrets(secret: Path) -> bool:
|
def filter_machine_secrets(secret: Path) -> bool:
|
||||||
return secret.joinpath("machines", name).exists()
|
return secret.joinpath("machines", machine).exists()
|
||||||
|
|
||||||
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(
|
||||||
paths,
|
paths,
|
||||||
flake_dir,
|
flake_dir,
|
||||||
f"Add machine {name} to secrets",
|
f"Add machine {machine} to secrets",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -70,9 +70,9 @@ def list_sops_machines(flake_dir: Path) -> list[str]:
|
|||||||
return list_objects(path, validate)
|
return list_objects(path, validate)
|
||||||
|
|
||||||
|
|
||||||
def add_secret(flake_dir: Path, machine: str, secret: str) -> None:
|
def add_secret(flake_dir: Path, machine: str, secret_path: Path) -> None:
|
||||||
paths = secrets.allow_member(
|
paths = secrets.allow_member(
|
||||||
secrets.machines_folder(sops_secrets_folder(flake_dir) / secret),
|
secrets.machines_folder(secret_path),
|
||||||
sops_machines_folder(flake_dir),
|
sops_machines_folder(flake_dir),
|
||||||
machine,
|
machine,
|
||||||
)
|
)
|
||||||
@@ -128,7 +128,11 @@ def add_secret_command(args: argparse.Namespace) -> None:
|
|||||||
if args.flake is None:
|
if args.flake is None:
|
||||||
msg = "Could not find clan flake toplevel directory"
|
msg = "Could not find clan flake toplevel directory"
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
add_secret(args.flake.path, args.machine, args.secret)
|
add_secret(
|
||||||
|
args.flake.path,
|
||||||
|
args.machine,
|
||||||
|
sops_secrets_folder(args.flake.path) / args.secret,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def remove_secret_command(args: argparse.Namespace) -> None:
|
def remove_secret_command(args: argparse.Namespace) -> None:
|
||||||
|
|||||||
@@ -210,15 +210,18 @@ def allow_member(
|
|||||||
msg += list_directory(source_folder)
|
msg += list_directory(source_folder)
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
group_folder.mkdir(parents=True, exist_ok=True)
|
group_folder.mkdir(parents=True, exist_ok=True)
|
||||||
user_target = group_folder / name
|
member = group_folder / name
|
||||||
if user_target.exists():
|
if member.exists():
|
||||||
if not user_target.is_symlink():
|
if not member.is_symlink():
|
||||||
msg = f"Cannot add user '{name}' to {group_folder.parent.name} secret. {user_target} exists but is not a symlink"
|
msg = f"Cannot add user '{name}' to {group_folder.parent.name} secret. {member} exists but is not a symlink"
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
user_target.unlink()
|
# return early if the symlink already points to the correct target
|
||||||
|
if member.resolve() == source:
|
||||||
|
return []
|
||||||
|
member.unlink()
|
||||||
|
|
||||||
user_target.symlink_to(os.path.relpath(source, user_target.parent))
|
member.symlink_to(os.path.relpath(source, member.parent))
|
||||||
changed = [user_target]
|
changed = [member]
|
||||||
if do_update_keys:
|
if do_update_keys:
|
||||||
changed.extend(
|
changed.extend(
|
||||||
update_keys(
|
update_keys(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from clan_cli.machines.machines import Machine
|
from clan_cli.machines.machines import Machine
|
||||||
from clan_cli.secrets.folders import sops_secrets_folder
|
from clan_cli.secrets.folders import sops_secrets_folder
|
||||||
from clan_cli.secrets.machines import add_machine, has_machine
|
from clan_cli.secrets.machines import add_machine, add_secret, has_machine
|
||||||
from clan_cli.secrets.secrets import decrypt_secret, encrypt_secret, has_secret
|
from clan_cli.secrets.secrets import decrypt_secret, encrypt_secret, has_secret
|
||||||
from clan_cli.secrets.sops import generate_private_key
|
from clan_cli.secrets.sops import generate_private_key
|
||||||
|
|
||||||
@@ -80,4 +80,9 @@ class SecretStore(SecretStoreBase):
|
|||||||
(output_dir / "key.txt").write_text(key)
|
(output_dir / "key.txt").write_text(key)
|
||||||
|
|
||||||
def exists(self, generator_name: str, name: str, shared: bool = False) -> bool:
|
def exists(self, generator_name: str, name: str, shared: bool = False) -> bool:
|
||||||
return (self.directory(generator_name, name, shared) / "secret").exists()
|
secret_folder = self.secret_path(generator_name, name, shared)
|
||||||
|
if not (secret_folder / "secret").exists():
|
||||||
|
return False
|
||||||
|
# add_secret will be a no-op if the machine is already added
|
||||||
|
add_secret(self.machine.flake_dir, self.machine.name, secret_folder)
|
||||||
|
return True
|
||||||
|
|||||||
@@ -17,69 +17,102 @@ def test_vm_deployment(
|
|||||||
temporary_home: Path,
|
temporary_home: Path,
|
||||||
sops_setup: SopsSetup,
|
sops_setup: SopsSetup,
|
||||||
) -> None:
|
) -> None:
|
||||||
config = nested_dict()
|
# machine 1
|
||||||
config["clan"]["virtualisation"]["graphics"] = False
|
machine1_config = nested_dict()
|
||||||
config["services"]["getty"]["autologinUser"] = "root"
|
machine1_config["clan"]["virtualisation"]["graphics"] = False
|
||||||
config["services"]["openssh"]["enable"] = True
|
machine1_config["services"]["getty"]["autologinUser"] = "root"
|
||||||
config["networking"]["firewall"]["enable"] = False
|
machine1_config["services"]["openssh"]["enable"] = True
|
||||||
my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"]
|
machine1_config["networking"]["firewall"]["enable"] = False
|
||||||
my_generator["files"]["my_secret"]["secret"] = True
|
m1_generator = machine1_config["clan"]["core"]["vars"]["generators"]["m1_generator"]
|
||||||
my_generator["script"] = """
|
m1_generator["files"]["my_secret"]["secret"] = True
|
||||||
|
m1_generator["script"] = """
|
||||||
echo hello > $out/my_secret
|
echo hello > $out/my_secret
|
||||||
"""
|
"""
|
||||||
my_shared_generator = config["clan"]["core"]["vars"]["generators"][
|
m1_shared_generator = machine1_config["clan"]["core"]["vars"]["generators"][
|
||||||
"my_shared_generator"
|
"my_shared_generator"
|
||||||
]
|
]
|
||||||
my_shared_generator["share"] = True
|
m1_shared_generator["share"] = True
|
||||||
my_shared_generator["files"]["shared_secret"]["secret"] = True
|
m1_shared_generator["files"]["shared_secret"]["secret"] = True
|
||||||
my_shared_generator["files"]["no_deploy_secret"]["secret"] = True
|
m1_shared_generator["files"]["no_deploy_secret"]["secret"] = True
|
||||||
my_shared_generator["files"]["no_deploy_secret"]["deploy"] = False
|
m1_shared_generator["files"]["no_deploy_secret"]["deploy"] = False
|
||||||
my_shared_generator["script"] = """
|
m1_shared_generator["script"] = """
|
||||||
echo hello > $out/shared_secret
|
echo hello > $out/shared_secret
|
||||||
echo hello > $out/no_deploy_secret
|
echo hello > $out/no_deploy_secret
|
||||||
"""
|
"""
|
||||||
|
# machine 2
|
||||||
|
machine2_config = nested_dict()
|
||||||
|
machine2_config["clan"]["virtualisation"]["graphics"] = False
|
||||||
|
machine2_config["services"]["getty"]["autologinUser"] = "root"
|
||||||
|
machine2_config["services"]["openssh"]["enable"] = True
|
||||||
|
machine2_config["users"]["users"]["root"]["openssh"]["authorizedKeys"]["keys"] = [
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDuhpzDHBPvn8nv8RH1MRomDOaXyP4GziQm7r3MZ1Syk grmpf"
|
||||||
|
]
|
||||||
|
machine2_config["networking"]["firewall"]["enable"] = False
|
||||||
|
machine2_config["clan"]["core"]["vars"]["generators"]["my_shared_generator"] = (
|
||||||
|
m1_shared_generator.copy()
|
||||||
|
)
|
||||||
|
|
||||||
flake = generate_flake(
|
flake = generate_flake(
|
||||||
temporary_home,
|
temporary_home,
|
||||||
flake_template=CLAN_CORE / "templates" / "minimal",
|
flake_template=CLAN_CORE / "templates" / "minimal",
|
||||||
machine_configs={"my_machine": config},
|
machine_configs={"m1_machine": machine1_config, "m2_machine": machine2_config},
|
||||||
monkeypatch=monkeypatch,
|
monkeypatch=monkeypatch,
|
||||||
)
|
)
|
||||||
monkeypatch.chdir(flake.path)
|
monkeypatch.chdir(flake.path)
|
||||||
sops_setup.init()
|
sops_setup.init()
|
||||||
cli.run(["vars", "generate", "my_machine"])
|
cli.run(["vars", "generate"])
|
||||||
# check sops secrets not empty
|
# check sops secrets not empty
|
||||||
sops_secrets = json.loads(
|
for machine in ["m1_machine", "m2_machine"]:
|
||||||
run(
|
sops_secrets = json.loads(
|
||||||
nix_eval(
|
run(
|
||||||
[
|
nix_eval(
|
||||||
f"{flake.path}#nixosConfigurations.my_machine.config.sops.secrets",
|
[
|
||||||
]
|
f"{flake.path}#nixosConfigurations.{machine}.config.sops.secrets",
|
||||||
)
|
]
|
||||||
).stdout.strip()
|
)
|
||||||
)
|
).stdout.strip()
|
||||||
assert sops_secrets != {}
|
)
|
||||||
|
assert sops_secrets != {}
|
||||||
my_secret_path = run(
|
my_secret_path = run(
|
||||||
nix_eval(
|
nix_eval(
|
||||||
[
|
[
|
||||||
f"{flake.path}#nixosConfigurations.my_machine.config.clan.core.vars.generators.my_generator.files.my_secret.path",
|
f"{flake.path}#nixosConfigurations.m1_machine.config.clan.core.vars.generators.m1_generator.files.my_secret.path",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
).stdout.strip()
|
).stdout.strip()
|
||||||
assert "no-such-path" not in my_secret_path
|
assert "no-such-path" not in my_secret_path
|
||||||
vm = run_vm_in_thread("my_machine")
|
for machine in ["m1_machine", "m2_machine"]:
|
||||||
qga = qga_connect("my_machine", vm)
|
shared_secret_path = run(
|
||||||
|
nix_eval(
|
||||||
|
[
|
||||||
|
f"{flake.path}#nixosConfigurations.{machine}.config.clan.core.vars.generators.my_shared_generator.files.shared_secret.path",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
).stdout.strip()
|
||||||
|
assert "no-such-path" not in shared_secret_path
|
||||||
|
vm_m1 = run_vm_in_thread("m1_machine")
|
||||||
|
vm_m2 = run_vm_in_thread("m2_machine", ssh_port=2222)
|
||||||
|
qga_m1 = qga_connect("m1_machine", vm_m1)
|
||||||
|
qga_m2 = qga_connect("m2_machine", vm_m2)
|
||||||
# check my_secret is deployed
|
# check my_secret is deployed
|
||||||
_, out, _ = qga.run("cat /run/secrets/vars/my_generator/my_secret", check=True)
|
_, out, _ = qga_m1.run("cat /run/secrets/vars/m1_generator/my_secret", check=True)
|
||||||
assert out == "hello\n"
|
assert out == "hello\n"
|
||||||
# check shared_secret is deployed
|
# check shared_secret is deployed on m1
|
||||||
_, out, _ = qga.run(
|
_, out, _ = qga_m1.run(
|
||||||
|
"cat /run/secrets/vars/my_shared_generator/shared_secret", check=True
|
||||||
|
)
|
||||||
|
assert out == "hello\n"
|
||||||
|
# check shared_secret is deployed on m2
|
||||||
|
_, out, _ = qga_m2.run(
|
||||||
"cat /run/secrets/vars/my_shared_generator/shared_secret", check=True
|
"cat /run/secrets/vars/my_shared_generator/shared_secret", check=True
|
||||||
)
|
)
|
||||||
assert out == "hello\n"
|
assert out == "hello\n"
|
||||||
# check no_deploy_secret is not deployed
|
# check no_deploy_secret is not deployed
|
||||||
returncode, out, _ = qga.run(
|
returncode, out, _ = qga_m1.run(
|
||||||
"test -e /run/secrets/vars/my_shared_generator/no_deploy_secret", check=False
|
"test -e /run/secrets/vars/my_shared_generator/no_deploy_secret", check=False
|
||||||
)
|
)
|
||||||
assert returncode != 0
|
assert returncode != 0
|
||||||
qga.exec_cmd("poweroff")
|
qga_m1.exec_cmd("poweroff")
|
||||||
wait_vm_down("my_machine", vm)
|
qga_m2.exec_cmd("poweroff")
|
||||||
|
wait_vm_down("m1_machine", vm_m1)
|
||||||
|
wait_vm_down("m2_machine", vm_m2)
|
||||||
|
|||||||
Reference in New Issue
Block a user