Files
Alexander 908c37f73f Add NixOS module for full agregators stack with VM test
Module manages PostgreSQL (both databases + schema init), qBittorrent
(VPN-confined via VPN-Confinement), Jackett, metadata-agregator,
musicfs, and the main orchestrator. All services are independently
enableable with auto-wired inter-service configuration.

Includes NixOS VM test validating PostgreSQL setup, schema
initialization, service startup, and directory creation.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/claude-agent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-20 12:39:04 +02:00

238 lines
6.6 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.agregators;
pgCfg = cfg.postgres;
# For local: run as the db user via peer auth (service runs as that user)
# For remote: run as anyone, use password auth via PGPASSWORD
mkPsql =
db:
lib.concatStringsSep " " (
[
"${pkgs.postgresql}/bin/psql"
]
++ lib.optionals (!isLocal) [
"-h ${lib.escapeShellArg pgCfg.host}"
"-p ${toString pgCfg.port}"
"-U ${lib.escapeShellArg db.user}"
]
++ [
"-d ${lib.escapeShellArg db.name}"
]
);
mkPasswordExport =
db:
if db.passwordFile != null then
''export PGPASSWORD="$(cat ${lib.escapeShellArg db.passwordFile})"''
else
"";
isLocal = pgCfg.host == "/run/postgresql" || pgCfg.host == "localhost" || pgCfg.host == "127.0.0.1";
in
{
options.services.agregators.postgres = {
host = lib.mkOption {
type = lib.types.str;
default = "/run/postgresql";
description = ''
PostgreSQL host. Use a path for unix socket (e.g. /run/postgresql)
or a hostname for TCP.
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 5432;
description = "PostgreSQL port (ignored for unix socket connections).";
};
musicDatabase = {
name = lib.mkOption {
type = lib.types.str;
default = "music_agregator";
description = "Database name for music-agregator.";
};
user = lib.mkOption {
type = lib.types.str;
default = "music_agregator";
description = "Database user for music-agregator.";
};
passwordFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "Password file for music-agregator database. Null for peer auth.";
};
};
metadataDatabase = {
name = lib.mkOption {
type = lib.types.str;
default = "metadata_agregator";
description = "Database name for metadata-agregator.";
};
user = lib.mkOption {
type = lib.types.str;
default = "metadata_agregator";
description = "Database user for metadata-agregator.";
};
passwordFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "Password file for metadata-agregator database. Null for peer auth.";
};
};
settings = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
description = "Extra PostgreSQL settings merged into services.postgresql.settings.";
example = {
shared_buffers = "256MB";
effective_cache_size = "768MB";
};
};
};
config = lib.mkIf cfg.enable {
# Create system users for peer auth (local postgres only)
users.users = lib.mkIf isLocal {
${pgCfg.musicDatabase.user} = {
isSystemUser = true;
group = cfg.group;
};
${pgCfg.metadataDatabase.user} = {
isSystemUser = true;
group = cfg.group;
};
};
# Enable and configure local PostgreSQL
services.postgresql = lib.mkIf isLocal {
enable = lib.mkDefault true;
settings = {
shared_preload_libraries = "pg_prewarm";
}
// pgCfg.settings;
ensureDatabases = [
pgCfg.musicDatabase.name
pgCfg.metadataDatabase.name
];
ensureUsers = [
{
name = pgCfg.musicDatabase.user;
ensureDBOwnership = true;
}
{
name = pgCfg.metadataDatabase.user;
ensureDBOwnership = true;
}
];
};
# Schema init for metadata_agregator
systemd.services.agregators-schema-metadata = {
description = "Initialize metadata_agregator database schema";
after = [ "network.target" ] ++ lib.optionals isLocal [ "postgresql.service" ];
requires = lib.optionals isLocal [ "postgresql.service" ];
before = [ "metadata-agregator.service" ];
requiredBy = [ "metadata-agregator.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
# Run as the db user for peer auth on local postgres
User = if isLocal then pgCfg.metadataDatabase.user else cfg.user;
};
script =
let
psql = mkPsql pgCfg.metadataDatabase;
passwordExport = mkPasswordExport pgCfg.metadataDatabase;
in
''
${passwordExport}
MARKER="${cfg.stateDir}/postgres/.schema-metadata-initialized"
# Wait for database
for i in $(seq 1 30); do
if ${psql} -c "SELECT 1" >/dev/null 2>&1; then break; fi
echo "Waiting for metadata database... ($i/30)"
sleep 1
done
if [ -f "$MARKER" ]; then
echo "metadata_agregator schema already initialized"
exit 0
fi
echo "Applying metadata_agregator schema..."
${psql} -f ${./sql/metadata/001_schema.sql}
touch "$MARKER"
echo "metadata_agregator schema initialized"
'';
};
# Schema init for music_agregator
systemd.services.agregators-schema-music = {
description = "Initialize music_agregator database schema";
after = [ "network.target" ] ++ lib.optionals isLocal [ "postgresql.service" ];
requires = lib.optionals isLocal [ "postgresql.service" ];
before = [ "music-agregator.service" ];
requiredBy = [ "music-agregator.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
# Run as the db user for peer auth on local postgres
User = if isLocal then pgCfg.musicDatabase.user else cfg.user;
};
script =
let
psql = mkPsql pgCfg.musicDatabase;
passwordExport = mkPasswordExport pgCfg.musicDatabase;
in
''
${passwordExport}
MARKER="${cfg.stateDir}/postgres/.schema-music-initialized"
# Wait for database
for i in $(seq 1 30); do
if ${psql} -c "SELECT 1" >/dev/null 2>&1; then break; fi
echo "Waiting for music database... ($i/30)"
sleep 1
done
if [ -f "$MARKER" ]; then
echo "music_agregator schema already initialized"
exit 0
fi
echo "Applying music_agregator schema..."
${psql} -f ${./sql/music/001_river.sql}
${psql} -f ${./sql/music/002_schema.sql}
${psql} -f ${./sql/music/003_event_bus.sql}
touch "$MARKER"
echo "music_agregator schema initialized"
'';
};
};
}