{ config, lib, pkgs, ... }: let cfg = config.services.federated-forge-discovery.indexer; yaml = pkgs.lib.generators.toYAML { }; # NixOS package for the indexer, injected via the flake. pkg = cfg.package; # Location where the rendered config is written. configDir = "/etc/ffd-indexer"; configFile = "${configDir}/config.yaml"; # The rendered YAML config, mirroring the config.go structure. nixConfig = { db_path = cfg.dbPath; typesense = { host = cfg.typesense.host; api_key = cfg.typesense.apiKey; } // lib.optionalAttrs (cfg.typesense.apiKeyFile != null) { # A secret file (sops/agenix) overrides the inline api_key in Go's # config resolution, so it also wins here. api_key_file = cfg.typesense.apiKeyFile; }; # The metrics daemon is an optional component: only render the block # when it is enabled, so an operator running just ffd-indexer can omit # the metrics section entirely. } // lib.optionalAttrs cfg.metrics.enable { metrics = { listen = cfg.metrics.listen; }; }; forges = cfg.forges; }; configYAML = pkgs.writeText "config.yaml" (yaml nixConfig); in { options.services.federated-forge-discovery.indexer = { enable = lib.mkEnableOption "Enables the Federated Forge Discovery Indexer systemd service and timer."; package = lib.mkOption { description = "Indexer package to use."; type = lib.types.package; # Provided by the flake overlay (see flake.nix overlays.default), so # `pkgs.ffd-indexer` resolves to this project's package. default = pkgs.ffd-indexer; }; schedule = lib.mkOption { description = '' How often the indexer runs. Either one of "daily", "hourly", "weekly", "monthly", or any systemd OnCalendar expression. ''; type = lib.types.str; default = "daily"; example = "daily"; }; dbPath = lib.mkOption { description = "Path to the SQLite database holding the index."; type = lib.types.path; default = "/var/lib/ffd-indexer/index.db"; example = "/var/lib/ffd-indexer/index.db"; }; typesense = { host = lib.mkOption { description = "Typesense server URL."; type = lib.types.str; default = "http://localhost:8108"; example = "http://localhost:8108"; }; apiKey = lib.mkOption { description = '' Typesense API key. May be inlined here, or supplied via a secret file with apiKeyFile (which takes precedence) ''; type = lib.types.str; default = ""; }; apiKeyFile = lib.mkOption { description = '' Path to a file containing the Typesense API key. Takes precedence over apiKey, so the key need not appear in the rendered config or the Nix store. ''; type = lib.types.nullOr lib.types.path; default = null; example = "/run/secrets/typesense-api-key"; }; }; metrics = { enable = lib.mkEnableOption "the ffd-metrics Prometheus daemon (also controls whether the metrics block is rendered into the config)"; listen = lib.mkOption { description = "Listen address the ffd-metrics daemon binds for /metrics."; type = lib.types.str; default = "127.0.0.1:9090"; example = "127.0.0.1:9090"; }; }; forges = lib.mkOption { description = '' Forge instances to scrape, as a list of "url" -> "type" entries, e.g. [{ url = "git.example.org"; type = "forgejo"; }]. The url is a bare hostname (FQDN); a scheme prefix (http:// or https://) is allowed to force the connection protocol, but the stored identity and metrics labels always use the bare hostname. The recommended type is a pinned "forgejo" or "gitea", which selects the scraper deterministically and skips probing. The default "auto" re-detects the type on every run and is EXPERIMENTAL: detection is probed via the version API, honors robots.txt (a forge that disallows /api/ is reported "unknown" unless hard-coded), and may misclassify until more detectors land. Prefer setting the type by hand. ''; type = lib.types.listOf ( lib.types.submodule { options = { url = lib.mkOption { description = "Forge hostname (FQDN) to scrape, e.g. \"git.example.org\"."; type = lib.types.str; }; type = lib.mkOption { description = '' Forge type. Recommended: "forgejo" or "gitea" (pin the scraper, skip detection). Default "auto" re-detects on every scan (EXPERIMENTAL). ''; type = lib.types.enum [ "auto" "forgejo" "gitea" ]; default = "auto"; }; }; } ); default = [ ]; example = [ { url = "git.example.org"; type = "forgejo"; } { url = "git.example.net"; type = "forgejo"; } ]; }; }; config = lib.mkIf cfg.enable { # Write the rendered config and make it readable by the service user. environment.etc."${lib.removePrefix "/etc/" configFile}" = { source = configYAML; user = "ffd-indexer"; group = "ffd-indexer"; mode = "0400"; }; users.users.ffd-indexer = { description = "Federated Forge Discovery Indexer"; isSystemUser = true; group = "ffd-indexer"; home = "/var/lib/ffd-indexer"; createHome = true; }; users.groups.ffd-indexer = { }; systemd.services.ffd-indexer = { description = "Federated Forge Discovery Indexer"; wantedBy = [ "multi-user.target" ]; wants = [ "network-online.target" ]; after = [ "network-online.target" ]; serviceConfig = { Type = "oneshot"; ExecStart = "${pkg}/bin/ffd-indexer -config ${configFile}"; User = "ffd-indexer"; Group = "ffd-indexer"; NoNewPrivileges = true; # The DB and Typesense data live under /var/lib/ffd-indexer. StateDirectory = "ffd-indexer"; Restart = "on-failure"; RestartSec = 30; }; }; # The timer drives the schedule; the service is oneshot. systemd.timers.ffd-indexer = { description = "Runs the Federated Forge Discovery Indexer on schedule"; wantedBy = [ "timers.target" ]; timerConfig = { OnCalendar = cfg.schedule; Persistent = true; Unit = "ffd-indexer.service"; }; }; # Long-running metrics daemon: reads the same DB the indexer writes to and # serves Prometheus metrics at the configured address. It is a separate # service so the indexer can stay a oneshot scheduled job, and it is # optional (disable metrics.enable to run only the indexer). systemd.services.ffd-metrics = lib.mkIf cfg.metrics.enable { description = "Federated Forge Discovery Indexer metrics daemon"; wantedBy = [ "multi-user.target" ]; wants = [ "network-online.target" ]; after = [ "network-online.target" "ffd-indexer.service" ]; serviceConfig = { Type = "simple"; ExecStart = "${pkg}/bin/ffd-metrics -config ${configFile}"; User = "ffd-indexer"; Group = "ffd-indexer"; NoNewPrivileges = true; StateDirectory = "ffd-indexer"; Restart = "on-failure"; RestartSec = 5; }; }; }; meta.maintainers = with lib.maintainers; [ ]; }