Merge pull request 'vars: implement secret generation' (#1731) from DavHau/clan-core:DavHau-vars into main
This commit is contained in:
@@ -36,7 +36,8 @@ in
|
||||
Each generator is expected to produce a set of files under a directory.
|
||||
'';
|
||||
default = { };
|
||||
type = attrsOf (submodule {
|
||||
type = attrsOf (
|
||||
submodule (generator: {
|
||||
imports = [ ./generator.nix ];
|
||||
options = options {
|
||||
dependencies = {
|
||||
@@ -65,6 +66,14 @@ in
|
||||
readOnly = true;
|
||||
default = file.config._module.args.name;
|
||||
};
|
||||
generatorName = {
|
||||
type = lib.types.str;
|
||||
description = ''
|
||||
name of the generator
|
||||
'';
|
||||
readOnly = true;
|
||||
default = generator.name;
|
||||
};
|
||||
secret = {
|
||||
description = ''
|
||||
Whether the file should be treated as a secret.
|
||||
@@ -157,7 +166,8 @@ in
|
||||
visible = false;
|
||||
};
|
||||
};
|
||||
});
|
||||
})
|
||||
);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ in
|
||||
fileModule = file: {
|
||||
path =
|
||||
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";
|
||||
};
|
||||
secretModule = "clan_cli.vars.secret_modules.sops";
|
||||
|
||||
@@ -14,12 +14,15 @@ class SecretStore(SecretStoreBase):
|
||||
self.machine = machine
|
||||
|
||||
# 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
|
||||
has_secrets = False
|
||||
for generator in self.machine.vars_generators.values():
|
||||
if "files" in generator:
|
||||
for file in generator["files"].values():
|
||||
if file["secret"]:
|
||||
has_secrets = True
|
||||
if not has_secrets:
|
||||
return
|
||||
|
||||
if has_machine(self.machine.flake_dir, self.machine.name):
|
||||
@@ -38,7 +41,7 @@ class SecretStore(SecretStoreBase):
|
||||
) -> Path | None:
|
||||
path = (
|
||||
sops_secrets_folder(self.machine.flake_dir)
|
||||
/ f"{self.machine.name}-{generator_name}-{name}"
|
||||
/ f"vars-{self.machine.name}-{generator_name}-{name}"
|
||||
)
|
||||
encrypt_secret(
|
||||
self.machine.flake_dir,
|
||||
@@ -49,15 +52,15 @@ class SecretStore(SecretStoreBase):
|
||||
)
|
||||
return path
|
||||
|
||||
def get(self, service: str, name: str) -> bytes:
|
||||
def get(self, generator_name: str, name: str) -> bytes:
|
||||
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")
|
||||
|
||||
def exists(self, service: str, name: str) -> bool:
|
||||
def exists(self, generator_name: str, name: str) -> bool:
|
||||
return has_secret(
|
||||
self.machine.flake_dir,
|
||||
f"{self.machine.name}-{name}",
|
||||
f"vars-{self.machine.name}-{generator_name}-{name}",
|
||||
)
|
||||
|
||||
def upload(self, output_dir: Path) -> None:
|
||||
|
||||
@@ -7,6 +7,11 @@ class KeyPair:
|
||||
self.privkey = privkey
|
||||
|
||||
|
||||
class SopsSetup:
|
||||
def __init__(self, keys: list[KeyPair]) -> None:
|
||||
self.keys = keys
|
||||
|
||||
|
||||
KEYS = [
|
||||
KeyPair(
|
||||
"age1dhwqzkah943xzc34tc3dlmfayyevcmdmxzjezdgdy33euxwf59vsp3vk3c",
|
||||
@@ -29,3 +34,14 @@ def age_keys() -> list[KeyPair]:
|
||||
Root directory of the tests
|
||||
"""
|
||||
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)
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
from age_keys import SopsSetup
|
||||
from fixtures_flakes import generate_flake
|
||||
from helpers import cli
|
||||
from root import CLAN_CORE
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.impure
|
||||
def test_generate_secret(
|
||||
def test_generate_public_var(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
temporary_home: Path,
|
||||
# age_keys: list["KeyPair"],
|
||||
@@ -41,8 +39,58 @@ def test_generate_secret(
|
||||
),
|
||||
)
|
||||
monkeypatch.chdir(flake.path)
|
||||
cmd = ["vars", "generate", "--flake", str(flake.path), "my_machine"]
|
||||
cli.run(cmd)
|
||||
cli.run(["vars", "generate", "--flake", str(flake.path), "my_machine"])
|
||||
assert (
|
||||
flake.path / "machines" / "my_machine" / "vars" / "my_generator" / "my_secret"
|
||||
).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()
|
||||
|
||||
Reference in New Issue
Block a user