refactor clanCore.secrets -> clanCore.facts
All checks were successful
checks / check-links (pull_request) Successful in 14s
checks / checks-impure (pull_request) Successful in 1m49s
checks / checks (pull_request) Successful in 3m33s

This commit is contained in:
lassulus 2024-03-25 15:55:25 +01:00 committed by Jörg Thalheim
parent 0676bf7283
commit a8d35d37e7
19 changed files with 342 additions and 165 deletions

View File

@ -74,7 +74,7 @@
}; };
}; };
}; };
clanCore.secretStore = "vm"; clanCore.facts.secretStore = "vm";
clanCore.clanDir = ../..; clanCore.clanDir = ../..;
environment.systemPackages = [ environment.systemPackages = [

View File

@ -36,7 +36,7 @@
}; };
}; };
}; };
clanCore.secretStore = "vm"; clanCore.facts.secretStore = "vm";
clan.borgbackup.destinations.test.repo = "borg@localhost:."; clan.borgbackup.destinations.test.repo = "borg@localhost:.";
} }

View File

@ -1,6 +1,7 @@
{ {
imports = [ imports = [
./backups.nix ./backups.nix
./facts
./manual.nix ./manual.nix
./imports.nix ./imports.nix
./metadata.nix ./metadata.nix
@ -10,7 +11,6 @@
./outputs.nix ./outputs.nix
./packages.nix ./packages.nix
./schema.nix ./schema.nix
./secrets
./vm.nix ./vm.nix
./wayland-proxy-virtwl.nix ./wayland-proxy-virtwl.nix
./zerotier ./zerotier

View File

@ -1,47 +1,44 @@
{ config, lib, ... }:
{ {
config, imports = [
lib, (lib.mkRemovedOptionModule [
pkgs, "clanCore"
... "secretsPrefix"
}: ] "secretsPrefix was only used by the sops module and the code is now integrated in there")
{ (lib.mkRenamedOptionModule
options.clanCore.secretStore = lib.mkOption { [
type = lib.types.enum [ "clanCore"
"sops" "secretStore"
"password-store" ]
"vm" [
"custom" "clanCore"
]; "facts"
default = "sops"; "secretStore"
description = '' ]
method to store secrets )
custom can be used to define a custom secret store. (lib.mkRenamedOptionModule
''; [
}; "clanCore"
"secretsDirectory"
options.clanCore.secretsDirectory = lib.mkOption { ]
type = lib.types.path; [
description = '' "clanCore"
The directory where secrets are installed to. This is backend specific. "facts"
''; "secretDirectory"
}; ]
)
options.clanCore.secretsUploadDirectory = lib.mkOption { (lib.mkRenamedOptionModule
type = lib.types.nullOr lib.types.path; [
default = null; "clanCore"
description = '' "secretsUploadDirectory"
The directory where secrets are uploaded into, This is backend specific. ]
''; [
}; "clanCore"
"facts"
options.clanCore.secretsPrefix = lib.mkOption { "secretUploadDirectory"
type = lib.types.str; ]
default = ""; )
description = '' ];
Prefix for secrets. This is backend specific.
'';
};
options.clanCore.secrets = lib.mkOption { options.clanCore.secrets = lib.mkOption {
default = { }; default = { };
type = lib.types.attrsOf ( type = lib.types.attrsOf (
@ -56,7 +53,7 @@
}; };
generator = lib.mkOption { generator = lib.mkOption {
type = lib.types.submodule ( type = lib.types.submodule (
{ config, ... }: { ... }:
{ {
options = { options = {
path = lib.mkOption { path = lib.mkOption {
@ -84,28 +81,6 @@
The script is expected to generate all secrets and facts defined in the module. The script is expected to generate all secrets and facts defined in the module.
''; '';
}; };
finalScript = lib.mkOption {
type = lib.types.str;
readOnly = true;
internal = true;
default = ''
set -eu -o pipefail
export PATH="${lib.makeBinPath config.path}:${pkgs.coreutils}/bin"
# prepare sandbox user
mkdir -p /etc
cp ${
pkgs.runCommand "fake-etc" { } ''
export PATH="${pkgs.coreutils}/bin"
mkdir -p $out
cp /etc/* $out/
''
}/* /etc/
${config.script}
'';
};
}; };
} }
); );
@ -134,11 +109,11 @@
description = '' description = ''
path to a secret which is generated by the generator path to a secret which is generated by the generator
''; '';
default = "${config'.clanCore.secretsDirectory}/${config'.clanCore.secretsPrefix}${config.name}"; default = "${config'.clanCore.facts.secretDirectory}/${config.name}";
defaultText = lib.literalExpression "\${config'.clanCore.secretsDirectory}/\${config'.clanCore.secretsPrefix}\${config.name}"; defaultText = lib.literalExpression "\${config'.clanCore.facts.secretDirectory}/\${config.name}";
}; };
} }
// lib.optionalAttrs (config'.clanCore.secretStore == "sops") { // lib.optionalAttrs (config'.clanCore.facts.secretStore == "sops") {
groups = lib.mkOption { groups = lib.mkOption {
type = lib.types.listOf lib.types.str; type = lib.types.listOf lib.types.str;
default = config'.clanCore.sops.defaultGroups; default = config'.clanCore.sops.defaultGroups;
@ -190,9 +165,16 @@
}) })
); );
}; };
imports = [ config = lib.mkIf (config.clanCore.secrets != { }) {
./sops.nix clanCore.facts.services = lib.mapAttrs' (
./password-store.nix name: service:
./vm.nix lib.warn "clanCore.secrets.${name} is deprecated, use clanCore.facts.services.${name} instead" (
]; lib.nameValuePair name ({
secret = service.secrets;
public = service.facts;
generator = service.generator;
})
)
) config.clanCore.secrets;
};
} }

View File

@ -0,0 +1,217 @@
{
config,
lib,
pkgs,
...
}:
{
options.clanCore.facts = {
secretStore = lib.mkOption {
type = lib.types.enum [
"sops"
"password-store"
"vm"
"custom"
];
default = "sops";
description = ''
method to store secret facts
custom can be used to define a custom secret fact store.
'';
};
secretModule = lib.mkOption {
type = lib.types.str;
internal = true;
description = ''
the python import path to the secret module
'';
};
secretDirectory = lib.mkOption {
type = lib.types.path;
description = ''
The directory where secrets are installed to. This is backend specific.
'';
};
secretUploadDirectory = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
The directory where secrets are uploaded into, This is backend specific.
'';
};
publicStore = lib.mkOption {
type = lib.types.enum [
"in_repo"
"vm"
"custom"
];
default = "in_repo";
description = ''
method to store public facts.
custom can be used to define a custom public fact store.
'';
};
publicModule = lib.mkOption {
type = lib.types.str;
internal = true;
description = ''
the python import path to the public module
'';
};
publicDirectory = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
};
services = lib.mkOption {
default = { };
type = lib.types.attrsOf (
lib.types.submodule (service: {
options = {
name = lib.mkOption {
type = lib.types.str;
default = service.config._module.args.name;
description = ''
Namespace of the service
'';
};
generator = lib.mkOption {
type = lib.types.submodule (
{ config, ... }:
{
options = {
path = lib.mkOption {
type = lib.types.listOf (lib.types.either lib.types.path lib.types.package);
default = [ ];
description = ''
Extra paths to add to the PATH environment variable when running the generator.
'';
};
prompt = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
prompt text to ask for a value.
This value will be passed to the script as the environment variable $prompt_value.
'';
};
script = lib.mkOption {
type = lib.types.str;
description = ''
Script to generate the secret.
The script will be called with the following variables:
- facts: path to a directory where facts can be stored
- secrets: path to a directory where secrets can be stored
The script is expected to generate all secrets and facts defined in the module.
'';
};
finalScript = lib.mkOption {
type = lib.types.str;
readOnly = true;
internal = true;
default = ''
set -eu -o pipefail
export PATH="${lib.makeBinPath config.path}:${pkgs.coreutils}/bin"
# prepare sandbox user
mkdir -p /etc
cp ${
pkgs.runCommand "fake-etc" { } ''
export PATH="${pkgs.coreutils}/bin"
mkdir -p $out
cp /etc/* $out/
''
}/* /etc/
${config.script}
'';
};
};
}
);
};
secret = lib.mkOption {
default = { };
type = lib.types.attrsOf (
lib.types.submodule (secret: {
options =
{
name = lib.mkOption {
type = lib.types.str;
description = ''
name of the secret
'';
default = secret.config._module.args.name;
};
path = lib.mkOption {
type = lib.types.str;
description = ''
path to a secret which is generated by the generator
'';
default = "${config.clanCore.facts.secretDirectory}/${secret.config.name}";
};
}
// lib.optionalAttrs (config.clanCore.facts.secretModule == "clan_cli.facts.secret_modules.sops") {
groups = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = config.clanCore.sops.defaultGroups;
description = ''
Groups to decrypt the secret for. By default we always use the user's key.
'';
};
};
})
);
description = ''
path where the secret is located in the filesystem
'';
};
public = lib.mkOption {
default = { };
type = lib.types.attrsOf (
lib.types.submodule (fact: {
options = {
name = lib.mkOption {
type = lib.types.str;
description = ''
name of the public fact
'';
default = fact.config._module.args.name;
};
path = lib.mkOption {
type = lib.types.path;
description = ''
path to a fact which is generated by the generator
'';
defaultText = lib.literalExpression "\${config.clanCore.clanDir}/machines/\${config.clanCore.machineName}/facts/\${fact.config.name}";
default =
config.clanCore.clanDir + "/machines/${config.clanCore.machineName}/facts/${fact.config.name}";
};
value = lib.mkOption {
defaultText = lib.literalExpression "\${config.clanCore.clanDir}/\${fact.config.path}";
type = lib.types.nullOr lib.types.str;
default =
if builtins.pathExists fact.config.path then lib.strings.fileContents fact.config.path else null;
};
};
})
);
};
};
})
);
};
};
imports = [
./compat.nix
./secret/sops.nix
./secret/password-store.nix
./secret/vm.nix
./public/in_repo.nix
./public/vm.nix
];
}

View File

@ -0,0 +1,6 @@
{ config, lib, ... }:
{
config = lib.mkIf (config.clanCore.facts.publicStore == "in_repo") {
clanCore.facts.publicModule = "clan_cli.facts.public_modules.in_repo";
};
}

View File

@ -0,0 +1,6 @@
{ config, lib, ... }:
{
config = lib.mkIf (config.clanCore.facts.publicStore == "vm") {
clanCore.facts.publicModule = "clan_cli.facts.public_modules.vm";
};
}

View File

@ -0,0 +1,15 @@
{ config, lib, ... }:
{
options.clan.password-store.targetDirectory = lib.mkOption {
type = lib.types.path;
default = "/etc/secrets";
description = ''
The directory where the password store is uploaded to.
'';
};
config = lib.mkIf (config.clanCore.facts.secretStore == "password-store") {
clanCore.facts.secretDirectory = config.clan.password-store.targetDirectory;
clanCore.facts.secretUploadDirectory = config.clan.password-store.targetDirectory;
clanCore.facts.secretModule = "clan_cli.facts.secret_modules.password_store";
};
}

View File

@ -41,10 +41,10 @@ in
description = "The default groups to for encryption use when no groups are specified."; description = "The default groups to for encryption use when no groups are specified.";
}; };
}; };
config = lib.mkIf (config.clanCore.secretStore == "sops") { config = lib.mkIf (config.clanCore.facts.secretStore == "sops") {
clanCore.secretsDirectory = "/run/secrets"; clanCore.facts.secretDirectory = "/run/secrets";
clanCore.secretsPrefix = config.clanCore.machineName + "-"; clanCore.facts.secretModule = "clan_cli.facts.secret_modules.sops";
system.clan.secretFactsModule = "clan_cli.facts.secret_modules.sops"; clanCore.facts.secretUploadDirectory = lib.mkDefault "/var/lib/sops-nix";
sops.secrets = builtins.mapAttrs (name: _: { sops.secrets = builtins.mapAttrs (name: _: {
sopsFile = config.clanCore.clanDir + "/sops/secrets/${name}/secret"; sopsFile = config.clanCore.clanDir + "/sops/secrets/${name}/secret";
format = "binary"; format = "binary";
@ -57,6 +57,5 @@ in
sops.age.keyFile = lib.mkIf (builtins.pathExists ( sops.age.keyFile = lib.mkIf (builtins.pathExists (
config.clanCore.clanDir + "/sops/secrets/${config.clanCore.machineName}-age.key/secret" config.clanCore.clanDir + "/sops/secrets/${config.clanCore.machineName}-age.key/secret"
)) (lib.mkDefault "/var/lib/sops-nix/key.txt"); )) (lib.mkDefault "/var/lib/sops-nix/key.txt");
clanCore.secretsUploadDirectory = lib.mkDefault "/var/lib/sops-nix";
}; };
} }

View File

@ -0,0 +1,8 @@
{ config, lib, ... }:
{
config = lib.mkIf (config.clanCore.facts.secretStore == "vm") {
clanCore.facts.secretDirectory = "/etc/secrets";
clanCore.facts.secretUploadDirectory = "/etc/secrets";
clanCore.facts.secretModule = "clan_cli.facts.secret_modules.vm";
};
}

View File

@ -23,6 +23,7 @@
}; };
clanDir = lib.mkOption { clanDir = lib.mkOption {
type = lib.types.either lib.types.path lib.types.str; type = lib.types.either lib.types.path lib.types.str;
default = ".";
description = '' description = ''
the location of the flake repo, used to calculate the location of facts and secrets the location of the flake repo, used to calculate the location of facts and secrets
''; '';

View File

@ -44,33 +44,6 @@
''; '';
default = false; default = false;
}; };
secretsUploadDirectory = lib.mkOption {
type = lib.types.path;
description = ''
the directory on the deployment server where secrets are uploaded
'';
};
publicFactsModule = lib.mkOption {
type = lib.types.str;
description = ''
the python import path to the facts module
'';
default = "clan_cli.facts.public_modules.in_repo";
};
secretFactsModule = lib.mkOption {
type = lib.types.str;
description = ''
the python import path to the secrets module
'';
default = "clan_cli.facts.secret_modules.sops";
};
secretsData = lib.mkOption {
type = lib.types.path;
description = ''
secret data as json for the generator
'';
default = pkgs.writers.writeJSON "secrets.json" config.clanCore.secrets;
};
vm.create = lib.mkOption { vm.create = lib.mkOption {
type = lib.types.path; type = lib.types.path;
description = '' description = ''
@ -92,10 +65,9 @@
# optimization for faster secret generate/upload and machines update # optimization for faster secret generate/upload and machines update
config = { config = {
system.clan.deployment.data = { system.clan.deployment.data = {
inherit (config.system.clan) publicFactsModule secretFactsModule secretsData; inherit (config.clanCore) facts;
inherit (config.clan.networking) targetHost buildHost; inherit (config.clan.networking) targetHost buildHost;
inherit (config.clan.deployment) requireExplicitUpdate; inherit (config.clan.deployment) requireExplicitUpdate;
inherit (config.clanCore) secretsUploadDirectory;
}; };
system.clan.deployment.file = pkgs.writeText "deployment.json" ( system.clan.deployment.file = pkgs.writeText "deployment.json" (
builtins.toJSON config.system.clan.deployment.data builtins.toJSON config.system.clan.deployment.data

View File

@ -1,15 +0,0 @@
{ config, lib, ... }:
{
options.clan.password-store.targetDirectory = lib.mkOption {
type = lib.types.path;
default = "/etc/secrets";
description = ''
The directory where the password store is uploaded to.
'';
};
config = lib.mkIf (config.clanCore.secretStore == "password-store") {
clanCore.secretsDirectory = config.clan.password-store.targetDirectory;
clanCore.secretsUploadDirectory = config.clan.password-store.targetDirectory;
system.clan.secretFactsModule = "clan_cli.facts.secret_modules.password_store";
};
}

View File

@ -1,9 +0,0 @@
{ config, lib, ... }:
{
config = lib.mkIf (config.clanCore.secretStore == "vm") {
clanCore.secretsDirectory = "/etc/secrets";
clanCore.secretsUploadDirectory = "/etc/secrets";
system.clan.secretFactsModule = "clan_cli.facts.secret_modules.vm";
system.clan.publicFactsModule = "clan_cli.facts.public_modules.vm";
};
}

View File

@ -18,9 +18,9 @@ def check_secrets(machine: Machine, service: None | str = None) -> bool:
if service: if service:
services = [service] services = [service]
else: else:
services = list(machine.secrets_data.keys()) services = list(machine.facts_data.keys())
for service in services: for service in services:
for secret_fact in machine.secrets_data[service]["secrets"]: for secret_fact in machine.facts_data[service]["secret"]:
if isinstance(secret_fact, str): if isinstance(secret_fact, str):
secret_name = secret_fact secret_name = secret_fact
else: else:
@ -31,7 +31,7 @@ def check_secrets(machine: Machine, service: None | str = None) -> bool:
) )
missing_secret_facts.append((service, secret_name)) missing_secret_facts.append((service, secret_name))
for public_fact in machine.secrets_data[service]["facts"]: for public_fact in machine.facts_data[service]["public"]:
if not public_facts_store.exists(service, public_fact): if not public_facts_store.exists(service, public_fact):
log.info( log.info(
f"Public fact '{public_fact}' for service {service} is missing." f"Public fact '{public_fact}' for service {service} is missing."

View File

@ -54,13 +54,13 @@ def generate_service_facts(
secrets_dir.mkdir(parents=True) secrets_dir.mkdir(parents=True)
env["secrets"] = str(secrets_dir) env["secrets"] = str(secrets_dir)
# compatibility for old outputs.nix users # compatibility for old outputs.nix users
if isinstance(machine.secrets_data[service]["generator"], str): if isinstance(machine.facts_data[service]["generator"], str):
generator = machine.secrets_data[service]["generator"] generator = machine.facts_data[service]["generator"]
else: else:
generator = machine.secrets_data[service]["generator"]["finalScript"] generator = machine.facts_data[service]["generator"]["finalScript"]
if machine.secrets_data[service]["generator"]["prompt"]: if machine.facts_data[service]["generator"]["prompt"]:
prompt_value = prompt( prompt_value = prompt(
machine.secrets_data[service]["generator"]["prompt"] machine.facts_data[service]["generator"]["prompt"]
) )
env["prompt_value"] = prompt_value env["prompt_value"] = prompt_value
# fmt: off # fmt: off
@ -90,7 +90,7 @@ def generate_service_facts(
) )
files_to_commit = [] files_to_commit = []
# store secrets # store secrets
for secret in machine.secrets_data[service]["secrets"]: for secret in machine.facts_data[service]["secret"]:
if isinstance(secret, str): if isinstance(secret, str):
# TODO: This is the old NixOS module, can be dropped everyone has updated. # TODO: This is the old NixOS module, can be dropped everyone has updated.
secret_name = secret secret_name = secret
@ -111,11 +111,11 @@ def generate_service_facts(
files_to_commit.append(secret_path) files_to_commit.append(secret_path)
# store facts # store facts
for name in machine.secrets_data[service]["facts"]: for name in machine.facts_data[service]["public"]:
fact_file = facts_dir / name fact_file = facts_dir / name
if not fact_file.is_file(): if not fact_file.is_file():
msg = f"did not generate a file for '{name}' when running the following command:\n" msg = f"did not generate a file for '{name}' when running the following command:\n"
msg += machine.secrets_data[service]["generator"] msg += machine.facts_data[service]["generator"]
raise ClanError(msg) raise ClanError(msg)
fact_file = public_facts_store.set(service, name, fact_file.read_bytes()) fact_file = public_facts_store.set(service, name, fact_file.read_bytes())
if fact_file: if fact_file:
@ -147,7 +147,7 @@ def generate_facts(
with TemporaryDirectory() as tmp: with TemporaryDirectory() as tmp:
tmpdir = Path(tmp) tmpdir = Path(tmp)
for service in machine.secrets_data: for service in machine.facts_data:
generate_service_facts( generate_service_facts(
machine=machine, machine=machine,
service=service, service=service,

View File

@ -106,8 +106,8 @@ class SecretStore(SecretStoreBase):
return local_hash.decode() == remote_hash return local_hash.decode() == remote_hash
def upload(self, output_dir: Path) -> None: def upload(self, output_dir: Path) -> None:
for service in self.machine.secrets_data: for service in self.machine.facts_data:
for secret in self.machine.secrets_data[service]["secrets"]: for secret in self.machine.facts_data[service]["secret"]:
if isinstance(secret, dict): if isinstance(secret, dict):
secret_name = secret["name"] secret_name = secret["name"]
else: else:

View File

@ -14,9 +14,9 @@ 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, "secrets_data"): if not hasattr(self.machine, "facts_data"):
return return
if not self.machine.secrets_data: if not self.machine.facts_data:
return return
if has_machine(self.machine.flake_dir, self.machine.name): if has_machine(self.machine.flake_dir, self.machine.name):

View File

@ -47,7 +47,7 @@ class Machine:
eval_cache: dict[str, str] eval_cache: dict[str, str]
build_cache: dict[str, Path] build_cache: dict[str, Path]
_flake_path: Path | None _flake_path: Path | None
_deployment_info: None | dict[str, str] _deployment_info: None | dict
vm: QMPWrapper vm: QMPWrapper
def __init__( def __init__(
@ -75,7 +75,7 @@ class Machine:
self.eval_cache: dict[str, str] = {} self.eval_cache: dict[str, str] = {}
self.build_cache: dict[str, Path] = {} self.build_cache: dict[str, Path] = {}
self._flake_path: Path | None = None self._flake_path: Path | None = None
self._deployment_info: None | dict[str, str] = deployment_info self._deployment_info: None | dict = deployment_info
state_dir = vm_state_dir(flake_url=str(self.flake), vm_name=self.data.name) state_dir = vm_state_dir(flake_url=str(self.flake), vm_name=self.data.name)
@ -88,7 +88,7 @@ class Machine:
return str(self) return str(self)
@property @property
def deployment_info(self) -> dict[str, str]: def deployment_info(self) -> dict:
if self._deployment_info is not None: if self._deployment_info is not None:
return self._deployment_info return self._deployment_info
self._deployment_info = json.loads( self._deployment_info = json.loads(
@ -113,26 +113,21 @@ class Machine:
@property @property
def secret_facts_module(self) -> str: def secret_facts_module(self) -> str:
return self.deployment_info["secretFactsModule"] return self.deployment_info["facts"]["secretModule"]
@property @property
def public_facts_module(self) -> str: def public_facts_module(self) -> str:
return self.deployment_info["publicFactsModule"] return self.deployment_info["facts"]["publicModule"]
@property @property
def secrets_data(self) -> dict[str, dict[str, Any]]: def facts_data(self) -> dict[str, dict[str, Any]]:
if self.deployment_info["secretsData"]: if self.deployment_info["facts"]["services"]:
try: return self.deployment_info["facts"]["services"]
return json.loads(Path(self.deployment_info["secretsData"]).read_text())
except json.JSONDecodeError as e:
raise ClanError(
f"Failed to parse secretsData for machine {self.data.name} as json"
) from e
return {} return {}
@property @property
def secrets_upload_directory(self) -> str: def secrets_upload_directory(self) -> str:
return self.deployment_info["secretsUploadDirectory"] return self.deployment_info["facts"]["secretUploadDirectory"]
@property @property
def flake_dir(self) -> Path: def flake_dir(self) -> Path: