vars: implement secret generation
This commit is contained in:
@@ -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;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
});
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user