{ config, lib, pkgs, ... }: let cfg = config.services.metadata-agregator; yaml = pkgs.formats.yaml { }; # Build the DSN from database options, password injected at runtime dbPasswordPlaceholder = "@DB_PASSWORD@"; configFile = yaml.generate "metadata-agregator.yaml" { server = { port = cfg.server.port; }; database = { host = cfg.database.host; port = cfg.database.port; name = cfg.database.name; user = cfg.database.user; password = dbPasswordPlaceholder; sslmode = cfg.database.sslmode; }; logging = { level = cfg.logging.level; format = cfg.logging.format; }; metrics = { enabled = cfg.metrics.enable; port = cfg.metrics.port; }; }; schemaFile = ./schema/001_schema.sql; in { options.services.metadata-agregator = { enable = lib.mkEnableOption "metadata-agregator music metadata gRPC service"; package = lib.mkPackageOption pkgs "metadata-agregator" { default = null; }; server = { port = lib.mkOption { type = lib.types.port; default = 50051; description = "gRPC listen port."; }; }; database = { host = lib.mkOption { type = lib.types.str; default = "localhost"; description = "PostgreSQL host."; }; port = lib.mkOption { type = lib.types.port; default = 5432; description = "PostgreSQL port."; }; name = lib.mkOption { type = lib.types.str; default = "metadata_agregator"; description = "Database name."; }; user = lib.mkOption { type = lib.types.str; default = "metadata_agregator"; description = "Database user."; }; passwordFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = '' File containing the database password. The file content is read at service start and injected into the config. Set to null for peer authentication (unix socket). ''; }; sslmode = lib.mkOption { type = lib.types.enum [ "disable" "require" "verify-ca" "verify-full" "prefer" "allow" ]; default = "disable"; description = "PostgreSQL SSL mode."; }; }; logging = { level = lib.mkOption { type = lib.types.str; default = "info"; description = "Log level."; }; format = lib.mkOption { type = lib.types.enum [ "json" "console" ]; default = "json"; description = "Log output format."; }; }; metrics = { enable = lib.mkOption { type = lib.types.bool; default = true; description = "Enable Prometheus metrics endpoint."; }; port = lib.mkOption { type = lib.types.port; default = 9090; description = "Prometheus metrics HTTP port."; }; }; user = lib.mkOption { type = lib.types.str; default = "metadata-agregator"; description = "User account under which the service runs."; }; group = lib.mkOption { type = lib.types.str; default = "metadata-agregator"; description = "Group under which the service runs."; }; openFirewall = lib.mkOption { type = lib.types.bool; default = false; description = "Whether to open the gRPC and metrics ports in the firewall."; }; }; config = lib.mkIf cfg.enable { users.users.${cfg.user} = lib.mkIf (cfg.user == "metadata-agregator") { isSystemUser = true; group = cfg.group; }; users.groups.${cfg.group} = lib.mkIf (cfg.group == "metadata-agregator") { }; # Schema init — idempotent oneshot that runs before the main service systemd.services.metadata-agregator-schema-init = { description = "Initialize metadata-agregator database schema"; after = [ "network.target" "postgresql.service" ]; before = [ "metadata-agregator.service" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; script = let passwordExport = if cfg.database.passwordFile != null then ''export PGPASSWORD="$(cat ${lib.escapeShellArg cfg.database.passwordFile})"'' else ""; psql = lib.concatStringsSep " " [ "${pkgs.postgresql}/bin/psql" "-h ${lib.escapeShellArg cfg.database.host}" "-p ${toString cfg.database.port}" "-U ${lib.escapeShellArg cfg.database.user}" "-d ${lib.escapeShellArg cfg.database.name}" ]; in '' ${passwordExport} # Wait for the database to accept connections for i in $(seq 1 30); do if ${psql} -c "SELECT 1" >/dev/null 2>&1; then break fi echo "Waiting for database... ($i/30)" sleep 1 done # Check if schema already exists (idempotent marker) if ${psql} -tAc "SELECT 1 FROM pg_tables WHERE schemaname='public' AND tablename='artists'" | grep -q 1; then echo "Schema already initialized, skipping" exit 0 fi echo "Applying metadata-agregator schema..." ${psql} -f ${schemaFile} echo "Schema initialized successfully" ''; }; # Main service systemd.services.metadata-agregator = { description = "metadata-agregator - Music metadata gRPC service"; after = [ "network.target" "metadata-agregator-schema-init.service" ]; requires = [ "metadata-agregator-schema-init.service" ]; wantedBy = [ "multi-user.target" ]; preStart = '' # Generate runtime config with password injected cp --no-preserve=mode ${configFile} /run/metadata-agregator/config.yaml ${ if cfg.database.passwordFile != null then '' ${pkgs.replace-secret}/bin/replace-secret \ '${dbPasswordPlaceholder}' \ '${cfg.database.passwordFile}' \ /run/metadata-agregator/config.yaml '' else '' ${pkgs.gnused}/bin/sed -i 's/${dbPasswordPlaceholder}//' /run/metadata-agregator/config.yaml '' } ''; serviceConfig = { Type = "simple"; ExecStart = "${lib.getExe cfg.package} -config /run/metadata-agregator/config.yaml"; User = cfg.user; Group = cfg.group; RuntimeDirectory = "metadata-agregator"; RuntimeDirectoryMode = "0750"; Restart = "on-failure"; RestartSec = 5; # Hardening ProtectSystem = "strict"; PrivateTmp = true; NoNewPrivileges = true; ProtectHome = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; }; }; networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall ( [ cfg.server.port ] ++ lib.optional cfg.metrics.enable cfg.metrics.port ); }; }