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>
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
{
|
||||
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"
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user