{ config, pkgs, lib, ... }: let cfg = config.services.tangled.knotmirror; in with lib; { options.services.tangled.knotmirror = { enable = mkOption { type = types.bool; default = false; description = "Enable a tangled knotmirror"; }; package = mkOption { type = types.package; description = "Package to use for the knotmirror"; }; v2 = { package = mkOption { type = types.package; description = "Package to use for the knotmirror/v2"; }; listenAddr = mkOption { type = types.str; default = "127.0.0.1:9001"; description = "Address to listen on"; }; logging = { endpoint = mkOption { type = types.str; default = ""; description = "Full OTLP/HTTP logs endpoint, e.g. http://prometheus:3100/otlp/v1/logs (empty disables export)"; }; serviceName = mkOption { type = types.str; default = "gitmirror"; description = "OpenTelemetry log service name"; }; }; }; tap-package = mkOption { type = types.package; description = "tap package to use for the knotmirror"; }; listenAddr = mkOption { type = types.str; default = "0.0.0.0:7000"; description = "Address to listen on"; }; adminListenAddr = mkOption { type = types.str; default = "127.0.0.1:7200"; description = "Address to listen on"; }; metricsListenAddr = mkOption { type = types.str; default = "0.0.0.0:7100"; description = "Listen address for the Prometheus metrics endpoint"; }; hostname = mkOption { type = types.str; example = "my.knotmirror.com"; description = "Hostname for the server (required)"; }; dbUrl = mkOption { type = types.str; example = "postgresql://..."; description = "Database URL. postgresql expected (required)"; }; deliberiUrl = mkOption { type = types.str; default = ""; description = "base URL for Deliberi XRPC calls"; }; deliberiDid = mkOption { type = types.str; default = ""; description = "Deliberi service DID used as the service-auth audience"; }; environmentFile = mkOption { type = types.nullOr types.path; default = null; description = "file with secret env vars such as MIRROR_SERVICE_PRIVATE_KEY"; }; atpPlcUrl = mkOption { type = types.str; default = "https://plc.directory"; description = "atproto PLC directory"; }; atpRelayUrl = mkOption { type = types.str; default = "https://relay1.us-east.bsky.network"; description = "atproto relay"; }; fullNetwork = mkOption { type = types.bool; default = false; description = "Whether to automatically mirror from entire network"; }; knotUseSSL = mkOption { type = types.bool; default = true; description = "Use SSL for knot connection"; }; knotSSRF = mkOption { type = types.bool; default = true; description = "enable SSRF protection for knots"; }; zoektUrl = mkOption { type = types.str; default = ""; example = "https://zoekt.tngl.boltless.dev/indexserver"; description = "zoekt-tnglserver url"; }; tap = { port = mkOption { type = types.port; default = 7480; description = "Internal port to run the knotmirror tap"; }; dbUrl = mkOption { type = types.str; default = "sqlite:///var/lib/knotmirror-tap/tap.db"; description = "database connection string (sqlite://path or postgres://...)"; }; }; }; config = mkIf cfg.enable { assertions = [ { assertion = (cfg.deliberiUrl == "") == (cfg.deliberiDid == ""); message = "services.tangled.knotmirror deliberiUrl and deliberiDid must be set together"; } { assertion = cfg.deliberiUrl == "" || cfg.environmentFile != null; message = "services.tangled.knotmirror environmentFile is required when Deliberi is configured"; } ]; environment.systemPackages = [ pkgs.git cfg.package ]; services.redis.servers.knotmirror = { enable = true; port = 6377; }; systemd.services.tap-knotmirror = { description = "knotmirror tap service"; after = ["network.target"]; wantedBy = ["multi-user.target"]; serviceConfig = { LogsDirectory = "knotmirror-tap"; StateDirectory = "knotmirror-tap"; Environment = [ "TAP_BIND=:${toString cfg.tap.port}" "TAP_PLC_URL=${cfg.atpPlcUrl}" "TAP_RELAY_URL=${cfg.atpRelayUrl}" "TAP_RESYNC_PARALLELISM=10" "TAP_DATABASE_URL=${cfg.tap.dbUrl}" "TAP_RETRY_TIMEOUT=60s" "TAP_COLLECTION_FILTERS=sh.tangled.repo" ( if cfg.fullNetwork then "TAP_SIGNAL_COLLECTION=sh.tangled.repo" else "TAP_FULL_NETWORK=false" ) ]; ExecStart = "${getExe cfg.tap-package} run"; }; }; systemd.services.knotmirror = { description = "knotmirror service"; after = ["network.target" "tap-knotmirror.service"]; wantedBy = ["multi-user.target"]; path = [ pkgs.git pkgs.util-linux ]; serviceConfig = { LogsDirectory = "knotmirror"; StateDirectory = "knotmirror"; EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; Environment = [ # TODO: add environment variables "MIRROR_LISTEN=${cfg.listenAddr}" "MIRROR_HOSTNAME=${cfg.hostname}" "MIRROR_TAP_URL=http://localhost:${toString cfg.tap.port}" "MIRROR_DB_URL=${cfg.dbUrl}" "MIRROR_REDIS_ADDR=localhost:6377" "MIRROR_GIT_BASEPATH=/var/lib/knotmirror/repos" "MIRROR_KNOT_USE_SSL=${boolToString cfg.knotUseSSL}" "MIRROR_KNOT_SSRF=${boolToString cfg.knotSSRF}" "MIRROR_RESYNC_PARALLELISM=12" "MIRROR_METRICS_LISTEN=${cfg.metricsListenAddr}" "MIRROR_ADMIN_LISTEN=${cfg.adminListenAddr}" "MIRROR_SLURPER_CONCURRENCY=4" "MIRROR_SEARCH_ZOEKT_URL=${cfg.zoektUrl}" "MIRROR_V2_HOST=http://${cfg.v2.listenAddr}" "MIRROR_DELIBERI_URL=${cfg.deliberiUrl}" "MIRROR_DELIBERI_DID=${cfg.deliberiDid}" ]; ExecStart = "${getExe cfg.package} serve"; Restart = "always"; }; }; systemd.services.gitmirror = { description = "gitmirror service"; after = ["network.target" "knotmirror.service"]; wantedBy = ["multi-user.target"]; serviceConfig = { LogsDirectory = "gitmirror"; StateDirectory = "gitmirror"; Environment = [ "GITMIRROR_BIND=${cfg.v2.listenAddr}" "GITMIRROR_SCAN_PATH=/var/lib/knotmirror/repos" "GITMIRROR_PLC_URL=${cfg.atpPlcUrl}" "GITMIRROR_SERVICE_DID=did:web:${cfg.hostname}" "GITMIRROR_LOG_ENDPOINT=${cfg.v2.logging.endpoint}" "GITMIRROR_LOG_SERVICE_NAME=${cfg.v2.logging.serviceName}" ]; ExecStart = "${getExe cfg.v2.package} serve"; Restart = "always"; }; }; }; }