{ lib, config, pkgs }: with lib; { mkPodmanService = { name, description, defaultPort ? "0", defaultSubdomain ? name, pod ? name, reverseProxied ? true, composeText, scheme ? "http", middlewares ? null, 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."traefik/rules/${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}"; after = dependencies; wantedBy = [ "multi-user.target" ]; onFailure = [ "service-failure-notify@%n.service" ]; startLimitBurst = 5; startLimitIntervalSec = 600; path = [ pkgs.podman pkgs.podman-compose pkgs.slirp4netns pkgs.su pkgs.sudo pkgs.coreutils ]; serviceConfig = { Type = "exec"; TimeoutStartSec = "1000"; ExecStartPre = [ "${pkgs.bash}/bin/bash -c 'sleep $((RANDOM % ${toString startDelay}))'" "${pkgs.bash}/bin/bash -c 'export PATH=/run/wrappers/bin:$PATH; exec ${pkgs.sudo}/bin/sudo -u numbus-admin podman-compose -f /etc/podman/${name}/compose.yaml pull'" ]; ExecStart = "${pkgs.bash}/bin/bash -c 'export PATH=/run/wrappers/bin:$PATH; exec ${pkgs.sudo}/bin/sudo -u numbus-admin podman-compose ${envFileArg} --in-pod ${toString pod} -f /etc/podman/${name}/compose.yaml up --remove-orphans'"; ExecStop = "${pkgs.bash}/bin/bash -c 'export PATH=/run/wrappers/bin:$PATH; exec ${pkgs.sudo}/bin/sudo -u numbus-admin podman-compose ${envFileArg} --in-pod ${toString pod} -f /etc/podman/${name}/compose.yaml down'"; Restart = "on-failure"; RestartSec = "3m"; }; }; 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 = "5m"; }; script = '' ${concatStringsSep "\n" (map (perm: '' set -- ${perm} WANTED_PERMISSIONS=$1 FOLDER_PATH=$2 if [[ ! -e "$FOLDER_PATH" ]]; then mkdir -p "$FOLDER_PATH" elif [[ ! -d "$FOLDER_PATH" ]]; then rm "$FOLDER_PATH" mkdir -p "$FOLDER_PATH" fi ACTUAL_PERMISSIONS=$(stat -c '%u:%g' "$FOLDER_PATH") if [[ "$ACTUAL_PERMISSIONS" != "$WANTED_PERMISSIONS" ]]; then chown -R "$WANTED_PERMISSIONS" "$FOLDER_PATH" 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 = "5m"; }; 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 ]); }; }