{ self }: { config, lib, pkgs, ... }: let listenerCfg = config.services.homelab-deploy.listener; builderCfg = config.services.homelab-deploy.builder; # Generate YAML config from settings generatedConfigFile = pkgs.writeText "builder.yaml" (lib.generators.toYAML {} { repos = lib.mapAttrs (name: repo: { url = repo.url; default_branch = repo.defaultBranch; }) builderCfg.settings.repos; }); # Use provided configFile or generate from settings builderConfigFile = if builderCfg.configFile != null then builderCfg.configFile else generatedConfigFile; # Build command line arguments for listener from configuration listenerArgs = lib.concatStringsSep " " ([ "--hostname ${lib.escapeShellArg listenerCfg.hostname}" "--tier ${listenerCfg.tier}" "--nats-url ${lib.escapeShellArg listenerCfg.natsUrl}" "--nkey-file ${lib.escapeShellArg listenerCfg.nkeyFile}" "--flake-url ${lib.escapeShellArg listenerCfg.flakeUrl}" "--timeout ${toString listenerCfg.timeout}" "--discover-subject ${lib.escapeShellArg listenerCfg.discoverSubject}" ] ++ lib.optional (listenerCfg.role != null) "--role ${lib.escapeShellArg listenerCfg.role}" ++ map (s: "--deploy-subject ${lib.escapeShellArg s}") listenerCfg.deploySubjects ++ lib.optionals listenerCfg.metrics.enable [ "--metrics-enabled" "--metrics-addr ${lib.escapeShellArg listenerCfg.metrics.address}" ]); # Build command line arguments for builder from configuration builderArgs = lib.concatStringsSep " " ([ "--nats-url ${lib.escapeShellArg builderCfg.natsUrl}" "--nkey-file ${lib.escapeShellArg builderCfg.nkeyFile}" "--config ${builderConfigFile}" "--timeout ${toString builderCfg.timeout}" ] ++ lib.optionals builderCfg.metrics.enable [ "--metrics-enabled" "--metrics-addr ${lib.escapeShellArg builderCfg.metrics.address}" ]); # Extract port from metrics address for firewall rule extractPort = addr: let # Handle both ":9972" and "0.0.0.0:9972" formats parts = lib.splitString ":" addr; in lib.toInt (lib.last parts); listenerMetricsPort = extractPort listenerCfg.metrics.address; builderMetricsPort = extractPort builderCfg.metrics.address; in { options.services.homelab-deploy.listener = { enable = lib.mkEnableOption "homelab-deploy listener service"; package = lib.mkOption { type = lib.types.package; default = self.packages.${pkgs.system}.homelab-deploy; description = "The homelab-deploy package to use"; }; hostname = lib.mkOption { type = lib.types.str; default = config.networking.hostName; description = "Hostname for this listener (used in subject templates)"; }; tier = lib.mkOption { type = lib.types.enum [ "test" "prod" ]; description = "Deployment tier for this host"; }; role = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "Role for role-based deployment targeting"; }; natsUrl = lib.mkOption { type = lib.types.str; description = "NATS server URL"; example = "nats://nats.example.com:4222"; }; nkeyFile = lib.mkOption { type = lib.types.path; description = "Path to NKey seed file for NATS authentication"; example = "/run/secrets/homelab-deploy-nkey"; }; flakeUrl = lib.mkOption { type = lib.types.str; description = "Git flake URL for nixos-rebuild"; example = "git+https://git.example.com/user/nixos-configs.git"; }; timeout = lib.mkOption { type = lib.types.int; default = 600; description = "Deployment timeout in seconds"; }; deploySubjects = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ "deploy.." "deploy..all" "deploy..role." ]; description = '' List of NATS subjects to subscribe to for deployment requests. Template variables: , , ''; }; discoverSubject = lib.mkOption { type = lib.types.str; default = "deploy.discover"; description = "NATS subject for host discovery requests"; }; environment = lib.mkOption { type = lib.types.attrsOf lib.types.str; default = { }; description = "Additional environment variables for the service"; example = { GIT_SSH_COMMAND = "ssh -i /run/secrets/deploy-key"; }; }; metrics = { enable = lib.mkEnableOption "Prometheus metrics endpoint"; address = lib.mkOption { type = lib.types.str; default = ":9972"; description = "Address for Prometheus metrics HTTP server"; example = "127.0.0.1:9972"; }; openFirewall = lib.mkOption { type = lib.types.bool; default = false; description = "Open firewall for metrics port"; }; }; }; options.services.homelab-deploy.builder = { enable = lib.mkEnableOption "homelab-deploy builder service"; package = lib.mkOption { type = lib.types.package; default = self.packages.${pkgs.system}.homelab-deploy; description = "The homelab-deploy package to use"; }; natsUrl = lib.mkOption { type = lib.types.str; description = "NATS server URL"; example = "nats://nats.example.com:4222"; }; nkeyFile = lib.mkOption { type = lib.types.path; description = "Path to NKey seed file for NATS authentication"; example = "/run/secrets/homelab-deploy-builder-nkey"; }; configFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = '' Path to builder configuration file (YAML). If not specified, a config file will be generated from the `settings` option. ''; example = "/etc/homelab-deploy/builder.yaml"; }; settings = { repos = lib.mkOption { type = lib.types.attrsOf (lib.types.submodule { options = { url = lib.mkOption { type = lib.types.str; description = "Git flake URL for the repository"; example = "git+https://git.example.com/org/nixos-configs.git"; }; defaultBranch = lib.mkOption { type = lib.types.str; default = "master"; description = "Default branch to build when not specified in request"; example = "main"; }; }; }); default = {}; description = '' Repository configuration for the builder. Each key is the repository name used in build requests. ''; example = lib.literalExpression '' { nixos-servers = { url = "git+https://git.example.com/org/nixos-servers.git"; defaultBranch = "master"; }; homelab = { url = "git+ssh://git@github.com/user/homelab.git"; defaultBranch = "main"; }; } ''; }; }; timeout = lib.mkOption { type = lib.types.int; default = 1800; description = "Build timeout in seconds per host"; }; environment = lib.mkOption { type = lib.types.attrsOf lib.types.str; default = { }; description = "Additional environment variables for the service"; example = { GIT_SSH_COMMAND = "ssh -i /run/secrets/deploy-key"; }; }; metrics = { enable = lib.mkEnableOption "Prometheus metrics endpoint"; address = lib.mkOption { type = lib.types.str; default = ":9973"; description = "Address for Prometheus metrics HTTP server"; example = "127.0.0.1:9973"; }; openFirewall = lib.mkOption { type = lib.types.bool; default = false; description = "Open firewall for metrics port"; }; }; }; config = lib.mkMerge [ (lib.mkIf builderCfg.enable { assertions = [ { assertion = builderCfg.configFile != null || builderCfg.settings.repos != {}; message = "services.homelab-deploy.builder: either configFile or settings.repos must be specified"; } ]; }) (lib.mkIf listenerCfg.enable { systemd.services.homelab-deploy-listener = { description = "homelab-deploy listener"; wantedBy = [ "multi-user.target" ]; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; # Prevent self-interruption during nixos-rebuild switch # The service will continue running the old version until manually restarted stopIfChanged = false; restartIfChanged = false; environment = listenerCfg.environment // { # Nix needs a writable cache for git flake fetching XDG_CACHE_HOME = "/var/cache/homelab-deploy"; }; path = [ pkgs.git config.system.build.nixos-rebuild ]; serviceConfig = { CacheDirectory = "homelab-deploy"; Type = "simple"; ExecStart = "${listenerCfg.package}/bin/homelab-deploy listener ${listenerArgs}"; Restart = "always"; RestartSec = 10; # Minimal hardening - nixos-rebuild requires broad system access: # - Write access to /nix/store for building # - Kernel namespace support for nix sandbox builds # - Ability to activate system configurations # - Network access for fetching from git/cache # Following the approach of nixos auto-upgrade which has no hardening }; }; networking.firewall.allowedTCPPorts = lib.mkIf (listenerCfg.metrics.enable && listenerCfg.metrics.openFirewall) [ listenerMetricsPort ]; }) (lib.mkIf builderCfg.enable { systemd.services.homelab-deploy-builder = { description = "homelab-deploy builder"; wantedBy = [ "multi-user.target" ]; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; environment = builderCfg.environment // { # Nix needs a writable cache for git flake fetching XDG_CACHE_HOME = "/var/cache/homelab-deploy-builder"; }; path = [ pkgs.git pkgs.nix ]; serviceConfig = { CacheDirectory = "homelab-deploy-builder"; Type = "simple"; ExecStart = "${builderCfg.package}/bin/homelab-deploy builder ${builderArgs}"; Restart = "always"; RestartSec = 10; # Minimal hardening - nix build requires broad system access: # - Write access to /nix/store for building # - Kernel namespace support for nix sandbox builds # - Network access for fetching from git/cache }; }; networking.firewall.allowedTCPPorts = lib.mkIf (builderCfg.metrics.enable && builderCfg.metrics.openFirewall) [ builderMetricsPort ]; }) ]; }