vars: introduce deploy=true/false for generated files

This commit is contained in:
DavHau
2024-09-01 14:30:13 +02:00
parent 2ca4fd29e4
commit ec055f7606
11 changed files with 69 additions and 17 deletions

View File

@@ -45,7 +45,11 @@ in
prompts prompts
share share
; ;
files = lib.flip lib.mapAttrs generator.files (_name: file: { inherit (file) secret; }); files = lib.flip lib.mapAttrs generator.files (
_name: file: {
inherit (file) deploy secret;
}
);
} }
); );
inherit (config.clan.core.vars.settings) secretUploadDirectory secretModule publicModule; inherit (config.clan.core.vars.settings) secretUploadDirectory secretModule publicModule;

View File

@@ -74,6 +74,15 @@ in
readOnly = true; readOnly = true;
default = generator.config._module.args.name; default = generator.config._module.args.name;
}; };
deploy = {
description = ''
Whether the file should be deployed to the target machine.
Enable this if the generated file is only used as an input to other generators.
'';
type = bool;
default = true;
};
secret = { secret = {
description = '' description = ''
Whether the file should be treated as a secret. Whether the file should be treated as a secret.

View File

@@ -6,17 +6,26 @@
}: }:
let let
inherit (lib) flip; inherit (lib) importJSON flip;
inherit (builtins) dirOf pathExists;
inherit (import ./funcs.nix { inherit lib; }) listVars; inherit (import ./funcs.nix { inherit lib; }) listVars;
inherit (config.clan.core) machineName; inherit (config.clan.core) machineName;
metaFile = sopsFile: dirOf sopsFile + "/meta.json";
metaData = sopsFile: if pathExists (metaFile sopsFile) then importJSON (metaFile sopsFile) else { };
toDeploy = secret: (metaData secret.sopsFile).deploy or true;
varsDirMachines = config.clan.core.clanDir + "/sops/vars/per-machine/${machineName}"; varsDirMachines = config.clan.core.clanDir + "/sops/vars/per-machine/${machineName}";
varsDirShared = config.clan.core.clanDir + "/sops/vars/shared"; varsDirShared = config.clan.core.clanDir + "/sops/vars/shared";
vars = (listVars varsDirMachines) ++ (listVars varsDirShared); vars' = (listVars varsDirMachines) ++ (listVars varsDirShared);
vars = lib.filter (secret: toDeploy secret) vars';
in in
{ {
config.clan.core.vars.settings = lib.mkIf (config.clan.core.vars.settings.secretStore == "sops") { config.clan.core.vars.settings = lib.mkIf (config.clan.core.vars.settings.secretStore == "sops") {

View File

@@ -87,9 +87,10 @@ def encrypt_secret(
add_users: list[str] = [], add_users: list[str] = [],
add_machines: list[str] = [], add_machines: list[str] = [],
add_groups: list[str] = [], add_groups: list[str] = [],
meta: dict = {},
) -> None: ) -> None:
key = ensure_sops_key(flake_dir) key = ensure_sops_key(flake_dir)
keys = set([]) recipient_keys = set([])
files_to_commit = [] files_to_commit = []
for user in add_users: for user in add_users:
@@ -122,10 +123,10 @@ def encrypt_secret(
) )
) )
keys = collect_keys_for_path(secret_path) recipient_keys = collect_keys_for_path(secret_path)
if key.pubkey not in keys: if key.pubkey not in recipient_keys:
keys.add(key.pubkey) recipient_keys.add(key.pubkey)
files_to_commit.extend( files_to_commit.extend(
allow_member( allow_member(
users_folder(secret_path), users_folder(secret_path),
@@ -136,7 +137,7 @@ def encrypt_secret(
) )
secret_path = secret_path / "secret" secret_path = secret_path / "secret"
encrypt_file(secret_path, value, list(sorted(keys))) encrypt_file(secret_path, value, list(sorted(recipient_keys)), meta)
files_to_commit.append(secret_path) files_to_commit.append(secret_path)
commit_files( commit_files(
files_to_commit, files_to_commit,

View File

@@ -143,12 +143,15 @@ def update_keys(secret_path: Path, keys: list[str]) -> list[Path]:
def encrypt_file( def encrypt_file(
secret_path: Path, content: IO[str] | str | bytes | None, keys: list[str] secret_path: Path,
content: IO[str] | str | bytes | None,
pubkeys: list[str],
meta: dict = {},
) -> None: ) -> None:
folder = secret_path.parent folder = secret_path.parent
folder.mkdir(parents=True, exist_ok=True) folder.mkdir(parents=True, exist_ok=True)
with sops_manifest(keys) as manifest: with sops_manifest(pubkeys) as manifest:
if not content: if not content:
args = ["sops", "--config", str(manifest)] args = ["sops", "--config", str(manifest)]
args.extend([str(secret_path)]) args.extend([str(secret_path)])
@@ -186,6 +189,9 @@ def encrypt_file(
with NamedTemporaryFile(dir=folder, delete=False) as f2: with NamedTemporaryFile(dir=folder, delete=False) as f2:
shutil.copyfile(f.name, f2.name) shutil.copyfile(f.name, f2.name)
os.rename(f2.name, secret_path) os.rename(f2.name, secret_path)
meta_path = secret_path.parent / "meta.json"
with open(meta_path, "w") as f_meta:
json.dump(meta, f_meta, indent=2)
finally: finally:
try: try:
os.remove(f.name) os.remove(f.name)
@@ -203,6 +209,14 @@ def decrypt_file(secret_path: Path) -> str:
return res.stdout return res.stdout
def get_meta(secret_path: Path) -> dict:
meta_path = secret_path.parent / "meta.json"
if not meta_path.exists():
return {}
with open(meta_path) as f:
return json.load(f)
def write_key(path: Path, publickey: str, overwrite: bool) -> None: def write_key(path: Path, publickey: str, overwrite: bool) -> None:
path.mkdir(parents=True, exist_ok=True) path.mkdir(parents=True, exist_ok=True)
try: try:

View File

@@ -152,6 +152,7 @@ def execute_generator(
# store secrets # store secrets
files = machine.vars_generators[generator_name]["files"] files = machine.vars_generators[generator_name]["files"]
for file_name, file in files.items(): for file_name, file in files.items():
is_deployed = file["deploy"]
groups = machine.deployment["sops"]["defaultGroups"] groups = machine.deployment["sops"]["defaultGroups"]
secret_file = tmpdir_out / file_name secret_file = tmpdir_out / file_name
@@ -166,6 +167,7 @@ def execute_generator(
secret_file.read_bytes(), secret_file.read_bytes(),
groups, groups,
shared=is_shared, shared=is_shared,
deployed=is_deployed,
) )
else: else:
file_path = public_vars_store.set( file_path = public_vars_store.set(

View File

@@ -17,6 +17,7 @@ class SecretStoreBase(ABC):
value: bytes, value: bytes,
groups: list[str], groups: list[str],
shared: bool = False, shared: bool = False,
deployed: bool = True,
) -> Path | None: ) -> Path | None:
pass pass

View File

@@ -31,6 +31,7 @@ class SecretStore(SecretStoreBase):
value: bytes, value: bytes,
groups: list[str], groups: list[str],
shared: bool = False, shared: bool = False,
deployed: bool = True,
) -> Path | None: ) -> Path | None:
subprocess.run( subprocess.run(
nix_shell( nix_shell(

View File

@@ -58,6 +58,7 @@ class SecretStore(SecretStoreBase):
value: bytes, value: bytes,
groups: list[str], groups: list[str],
shared: bool = False, shared: bool = False,
deployed: bool = True,
) -> Path | None: ) -> Path | None:
path = self.secret_path(generator_name, name, shared) path = self.secret_path(generator_name, name, shared)
encrypt_secret( encrypt_secret(
@@ -66,6 +67,9 @@ class SecretStore(SecretStoreBase):
value, value,
add_machines=[self.machine.name], add_machines=[self.machine.name],
add_groups=groups, add_groups=groups,
meta=dict(
deploy=deployed,
),
) )
return path return path

View File

@@ -21,6 +21,7 @@ class SecretStore(SecretStoreBase):
value: bytes, value: bytes,
groups: list[str], groups: list[str],
shared: bool = False, shared: bool = False,
deployed: bool = True,
) -> Path | None: ) -> Path | None:
secret_file = self.dir / service / name secret_file = self.dir / service / name
secret_file.parent.mkdir(parents=True, exist_ok=True) secret_file.parent.mkdir(parents=True, exist_ok=True)

View File

@@ -25,20 +25,19 @@ def test_vm_deployment(
config["networking"]["firewall"]["enable"] = False config["networking"]["firewall"]["enable"] = False
my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"] my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"]
my_generator["files"]["my_secret"]["secret"] = True my_generator["files"]["my_secret"]["secret"] = True
my_generator["files"]["my_value"]["secret"] = False
my_generator["script"] = """ my_generator["script"] = """
echo hello > $out/my_secret echo hello > $out/my_secret
echo hello > $out/my_value
""" """
my_shared_generator = config["clan"]["core"]["vars"]["generators"][ my_shared_generator = config["clan"]["core"]["vars"]["generators"][
"my_shared_generator" "my_shared_generator"
] ]
my_shared_generator["share"] = True my_shared_generator["share"] = True
my_shared_generator["files"]["my_shared_secret"]["secret"] = True my_shared_generator["files"]["shared_secret"]["secret"] = True
my_shared_generator["files"]["my_shared_value"]["secret"] = False my_shared_generator["files"]["no_deploy_secret"]["secret"] = True
my_shared_generator["files"]["no_deploy_secret"]["deploy"] = False
my_shared_generator["script"] = """ my_shared_generator["script"] = """
echo hello > $out/my_shared_secret echo hello > $out/shared_secret
echo hello > $out/my_shared_value echo hello > $out/no_deploy_secret
""" """
flake = generate_flake( flake = generate_flake(
temporary_home, temporary_home,
@@ -69,11 +68,18 @@ def test_vm_deployment(
assert "no-such-path" not in my_secret_path assert "no-such-path" not in my_secret_path
run_vm_in_thread("my_machine") run_vm_in_thread("my_machine")
qga = qga_connect("my_machine") qga = qga_connect("my_machine")
# check my_secret is deployed
_, out, _ = qga.run("cat /run/secrets/vars/my_generator/my_secret", check=True) _, out, _ = qga.run("cat /run/secrets/vars/my_generator/my_secret", check=True)
assert out == "hello\n" assert out == "hello\n"
# check shared_secret is deployed
_, out, _ = qga.run( _, out, _ = qga.run(
"cat /run/secrets/vars/my_shared_generator/my_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
returncode, out, _ = qga.run(
"test -e /run/secrets/vars/my_shared_generator/no_deploy_secret", check=False
)
assert returncode != 0
qga.exec_cmd("poweroff") qga.exec_cmd("poweroff")
wait_vm_down("my_machine") wait_vm_down("my_machine")