vars: implement secret generation

This commit is contained in:
DavHau
2024-07-10 17:26:47 +07:00
parent 2a245a6111
commit 7dbed61079
5 changed files with 204 additions and 127 deletions

View File

@@ -36,7 +36,8 @@ in
Each generator is expected to produce a set of files under a directory. Each generator is expected to produce a set of files under a directory.
''; '';
default = { }; default = { };
type = attrsOf (submodule { type = attrsOf (
submodule (generator: {
imports = [ ./generator.nix ]; imports = [ ./generator.nix ];
options = options { options = options {
dependencies = { dependencies = {
@@ -65,6 +66,14 @@ in
readOnly = true; readOnly = true;
default = file.config._module.args.name; default = file.config._module.args.name;
}; };
generatorName = {
type = lib.types.str;
description = ''
name of the generator
'';
readOnly = true;
default = generator.name;
};
secret = { secret = {
description = '' description = ''
Whether the file should be treated as a secret. Whether the file should be treated as a secret.
@@ -157,7 +166,8 @@ in
visible = false; visible = false;
}; };
}; };
}); })
);
}; };
}; };
} }

View File

@@ -38,7 +38,7 @@ in
fileModule = file: { fileModule = file: {
path = path =
lib.mkIf file.secret lib.mkIf file.secret
config.sops.secrets.${"${config.clan.core.machineName}-${file.config.name}"}.path config.sops.secrets.${"vars-${config.clan.core.machineName}-${file.config.generatorName}-${file.config.name}"}.path
or "/no-such-path"; or "/no-such-path";
}; };
secretModule = "clan_cli.vars.secret_modules.sops"; secretModule = "clan_cli.vars.secret_modules.sops";

View File

@@ -14,12 +14,15 @@ class SecretStore(SecretStoreBase):
self.machine = machine self.machine = machine
# 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, "vars_data") or not self.machine.vars_generators: if not self.machine.vars_generators:
return return
has_secrets = False
for generator in self.machine.vars_generators.values(): for generator in self.machine.vars_generators.values():
if "files" in generator: if "files" in generator:
for file in generator["files"].values(): for file in generator["files"].values():
if file["secret"]: if file["secret"]:
has_secrets = True
if not has_secrets:
return return
if has_machine(self.machine.flake_dir, self.machine.name): if has_machine(self.machine.flake_dir, self.machine.name):
@@ -38,7 +41,7 @@ class SecretStore(SecretStoreBase):
) -> Path | None: ) -> Path | None:
path = ( path = (
sops_secrets_folder(self.machine.flake_dir) sops_secrets_folder(self.machine.flake_dir)
/ f"{self.machine.name}-{generator_name}-{name}" / f"vars-{self.machine.name}-{generator_name}-{name}"
) )
encrypt_secret( encrypt_secret(
self.machine.flake_dir, self.machine.flake_dir,
@@ -49,15 +52,15 @@ class SecretStore(SecretStoreBase):
) )
return path return path
def get(self, service: str, name: str) -> bytes: def get(self, generator_name: str, name: str) -> bytes:
return decrypt_secret( return decrypt_secret(
self.machine.flake_dir, f"{self.machine.name}-{name}" self.machine.flake_dir, f"vars-{self.machine.name}-{generator_name}-{name}"
).encode("utf-8") ).encode("utf-8")
def exists(self, service: str, name: str) -> bool: def exists(self, generator_name: str, name: str) -> bool:
return has_secret( return has_secret(
self.machine.flake_dir, self.machine.flake_dir,
f"{self.machine.name}-{name}", f"vars-{self.machine.name}-{generator_name}-{name}",
) )
def upload(self, output_dir: Path) -> None: def upload(self, output_dir: Path) -> None:

View File

@@ -7,6 +7,11 @@ class KeyPair:
self.privkey = privkey self.privkey = privkey
class SopsSetup:
def __init__(self, keys: list[KeyPair]) -> None:
self.keys = keys
KEYS = [ KEYS = [
KeyPair( KeyPair(
"age1dhwqzkah943xzc34tc3dlmfayyevcmdmxzjezdgdy33euxwf59vsp3vk3c", "age1dhwqzkah943xzc34tc3dlmfayyevcmdmxzjezdgdy33euxwf59vsp3vk3c",
@@ -29,3 +34,14 @@ def age_keys() -> list[KeyPair]:
Root directory of the tests Root directory of the tests
""" """
return KEYS return KEYS
@pytest.fixture
def sops_setup(
monkeypatch: pytest.MonkeyPatch,
) -> SopsSetup:
"""
Root directory of the tests
"""
monkeypatch.setenv("SOPS_AGE_KEY", KEYS[0].privkey)
return SopsSetup(KEYS)

View File

@@ -1,17 +1,15 @@
import os
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING
import pytest import pytest
from age_keys import SopsSetup
from fixtures_flakes import generate_flake from fixtures_flakes import generate_flake
from helpers import cli from helpers import cli
from root import CLAN_CORE from root import CLAN_CORE
if TYPE_CHECKING:
pass
@pytest.mark.impure @pytest.mark.impure
def test_generate_secret( def test_generate_public_var(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
temporary_home: Path, temporary_home: Path,
# age_keys: list["KeyPair"], # age_keys: list["KeyPair"],
@@ -41,8 +39,58 @@ def test_generate_secret(
), ),
) )
monkeypatch.chdir(flake.path) monkeypatch.chdir(flake.path)
cmd = ["vars", "generate", "--flake", str(flake.path), "my_machine"] cli.run(["vars", "generate", "--flake", str(flake.path), "my_machine"])
cli.run(cmd)
assert ( assert (
flake.path / "machines" / "my_machine" / "vars" / "my_generator" / "my_secret" flake.path / "machines" / "my_machine" / "vars" / "my_generator" / "my_secret"
).is_file() ).is_file()
@pytest.mark.impure
def test_generate_secret_var(
monkeypatch: pytest.MonkeyPatch,
temporary_home: Path,
sops_setup: SopsSetup,
) -> None:
flake = generate_flake(
temporary_home,
flake_template=CLAN_CORE / "templates" / "minimal",
machine_configs=dict(
my_machine=dict(
clan=dict(
core=dict(
vars=dict(
generators=dict(
my_generator=dict(
files=dict(
my_secret=dict(
secret=True,
)
),
script="echo hello > $out/my_secret",
)
)
)
)
)
)
),
)
monkeypatch.chdir(flake.path)
cli.run(
[
"secrets",
"users",
"add",
"--flake",
str(flake.path),
os.environ.get("USER", "user"),
sops_setup.keys[0].pubkey,
]
)
cli.run(["vars", "generate", "--flake", str(flake.path), "my_machine"])
assert not (
flake.path / "machines" / "my_machine" / "vars" / "my_generator" / "my_secret"
).is_file()
assert (
flake.path / "sops" / "secrets" / "vars-my_machine-my_generator-my_secret"
).is_dir()