{ lib, config, pkgs }: with lib; { mkPodmanService = { name, description, defaultPort ? "0", defaultSubdomain ? name, pod ? name, reverseProxied ? true, composeText, scheme ? "http", middlewares ? [ "secureHeaders" ], dependencies ? [ "traefik.service" "${config.numbus.services.dns}.service" ], extraOptions ? {}, extraConfig ? {}, configDirEnabled ? true, dataDirEnabled ? true, startDelay ? 180, dirPermissions ? [], generatedSecrets ? {}, importedSecrets ? {}, envFile ? null, ... }: let cfg = config.numbus.services.${name}; hasSecrets = (generatedSecrets != {}) || (importedSecrets != {}); envFilePath = if envFile == null then "/var/lib/numbus-server/${name}/.env" else envFile; envFileArg = if hasSecrets || envFile != null then "--env-file ${envFilePath}" else ""; in { options.numbus.services.${name} = recursiveUpdate ({ enable = mkEnableOption description; subdomain = mkOption { type = types.str; default = defaultSubdomain; example = defaultSubdomain; description = "The subdomain that ${name} will use"; }; port = mkOption { type = types.str; default = defaultPort; example = defaultPort; description = "The port that ${name} will use."; }; reverseProxied = mkOption { type = types.bool; default = reverseProxied; example = reverseProxied; description = "Whether to create a Traefik reverse proxy configuration for this service."; }; } // (optionalAttrs configDirEnabled { configDir = mkOption { type = types.str; default = "/mnt/config/${name}"; example = "/mnt/config/${name}"; description = "The directory where ${name}'s configuration files will be stored"; }; }) // (optionalAttrs dataDirEnabled { dataDir = mkOption { type = types.str; default = "/mnt/data/${name}"; example = "/mnt/data/${name}"; description = "The directory where ${name}'s data will be stored"; }; })) extraOptions; config = mkIf cfg.enable (mkMerge [ { environment.etc."podman/${name}/compose.yaml".text = composeText; environment.etc."${config.numbus.traefikDynamicConfigDir}/${name}.yaml" = mkIf cfg.reverseProxied { text = '' http: routers: ${name}: rule: "Host(`${cfg.subdomain}.${config.numbus.services.domain}`)" entrypoints: - "websecure" service: ${name} middlewares: ${concatStringsSep "\n" (map (m: " - ${m}") middlewares)} tls: certresolver: "cloudflare" options: "secureTLS" services: ${name}: loadBalancer: servers: - url: "${scheme}://host.containers.internal:${cfg.port}" ''; }; systemd.services."${name}" = { description = "Podman container : ${name}"; requires = dependencies; after = dependencies; wantedBy = [ "multi-user.target" ]; onFailure = [ "service-failure-notify@%n.service" ]; startLimitBurst = 5; startLimitIntervalSec = 600; path = [ pkgs.podman pkgs.podman-compose pkgs.coreutils pkgs.sudo ]; serviceConfig = { Type = "exec"; ExecStartPre = [ "bash -c 'sleep $((RANDOM % ${toString startDelay}))'" "- sudo -u numbus-admin podman-compose ${envFileArg} -f /etc/podman/${name}/compose.yaml pull" ]; ExecStart = "sudo -u numbus-admin podman-compose ${envFileArg} --in-pod ${toString pod} -f /etc/podman/${name}/compose.yaml up --remove-orphans"; ExecStop = "sudo -u numbus-admin podman-compose ${envFileArg} --in-pod ${toString pod} -f /etc/podman/${name}/compose.yaml down"; Restart = "on-failure"; RestartSec = "1m"; }; }; systemd.services."${name}-permissions" = mkIf (dirPermissions != []) { description = "Podman container : ${name} : check and fix permissions"; before = [ "${name}.service" ]; wantedBy = [ "multi-user.target" "${name}.service" ]; onFailure = [ "service-failure-notify@%n.service" ]; startLimitBurst = 5; startLimitIntervalSec = 600; path = [ pkgs.coreutils ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; Restart = "on-failure"; RestartSec = "1m"; }; script = '' mkdir -p /var/lib/numbus-server/${name} ${concatStringsSep "\n" (map (perm: '' set -- ${perm} MARKER="/var/lib/numbus-server/${name}/.perm-fixed-$(echo "$1:$2" | md5sum | cut -d' ' -f1)" if [ ! -f "$MARKER" ]; then rm -f /var/lib/numbus-server/${name}/.perm-fixed-* mkdir -p "$2" chown -R "$1" "$2" touch "$MARKER" fi '') dirPermissions)} exit 0 ''; }; systemd.services."${name}-secrets" = mkIf hasSecrets { description = "Podman container create secrets : ${name}"; before = [ "${name}.service" ]; wantedBy = [ "multi-user.target" "${name}.service" ]; onFailure = [ "service-failure-notify@%n.service" ]; startLimitBurst = 5; startLimitIntervalSec = 600; path = [ pkgs.coreutils pkgs.xkcdpass pkgs.gnugrep ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; Restart = "on-failure"; RestartSec = "1m"; }; script = '' mkdir -p /var/lib/numbus-server/${name} SECRETS_FILE="${envFilePath}" if [[ ! -f "$SECRETS_FILE" ]]; then touch "$SECRETS_FILE" fi # Generated Secrets (only if missing) ${concatStringsSep "\n" (mapAttrsToList (k: v: '' if ! grep -q "^${k}=" "$SECRETS_FILE"; then echo "${k}=$(${v})" >> "$SECRETS_FILE" fi '') generatedSecrets)} # Imported Secrets (update or append) ${concatStringsSep "\n" (mapAttrsToList (k: v: '' if grep -q "^${k}=" "$SECRETS_FILE"; then grep -v "^${k}=" "$SECRETS_FILE" > "$SECRETS_FILE.tmp" mv "$SECRETS_FILE.tmp" "$SECRETS_FILE" fi echo "${k}=${lib.escapeShellArg v}" >> "$SECRETS_FILE" '') importedSecrets)} chown numbus-admin:users "$SECRETS_FILE" chmod 600 "$SECRETS_FILE" ''; }; } extraConfig ]); }; }