{ 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" ''; }; }; }