383 lines
12 KiB
Nix
Executable File
383 lines
12 KiB
Nix
Executable File
{
|
|
inputs,
|
|
lib,
|
|
namespace,
|
|
}:
|
|
let
|
|
inherit (inputs.nixpkgs.lib)
|
|
mapAttrs
|
|
mkOption
|
|
types
|
|
toUpper
|
|
substring
|
|
stringLength
|
|
mkDefault
|
|
mkForce
|
|
;
|
|
in
|
|
rec {
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# NixOS service module helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Create a NixOS module with standard options (enable, port, reverseProxy,
|
|
# firewall, user, postgresql, redis) and optional caller-supplied options and
|
|
# config. All config is gated behind `cfg.enable`.
|
|
mkModule =
|
|
{
|
|
name,
|
|
description ? "",
|
|
options ? { },
|
|
moduleConfig ? { },
|
|
domain ? "services",
|
|
config,
|
|
serviceName ? name,
|
|
}:
|
|
let
|
|
cfg = config.${namespace}.${domain}.${name};
|
|
|
|
upstreamUrl =
|
|
if cfg.reverseProxy.upstreamUrl != null then
|
|
cfg.reverseProxy.upstreamUrl
|
|
else
|
|
"http://127.0.0.1:${toString cfg.port}";
|
|
|
|
fqdn = "${cfg.reverseProxy.subdomain}.${cfg.reverseProxy.domain}";
|
|
|
|
defaultConfig = {
|
|
# Caddy reverse proxy: when reverseProxy.enable = true, contribute this
|
|
# service's named-matcher block into the shared wildcard virtual host.
|
|
services.caddy.virtualHosts."*.${cfg.reverseProxy.domain}" = lib.mkIf cfg.reverseProxy.enable {
|
|
extraConfig = ''
|
|
@${name} host ${fqdn}
|
|
handle @${name} {
|
|
reverse_proxy ${upstreamUrl}
|
|
${cfg.reverseProxy.extraCaddyConfig}
|
|
}
|
|
'';
|
|
};
|
|
|
|
networking.firewall = lib.mkIf cfg.openFirewall {
|
|
allowedTCPPorts = [ cfg.port ];
|
|
allowedUDPPorts = [ cfg.port ];
|
|
};
|
|
|
|
users = lib.mkIf cfg.createUser {
|
|
users.${name} = {
|
|
isSystemUser = true;
|
|
group = name;
|
|
home = cfg.configDir;
|
|
};
|
|
groups.${name} = { };
|
|
};
|
|
|
|
# RequiresMountsFor is silently ignored when the paths live on the root
|
|
# filesystem, so this is safe on non-NAS hosts too.
|
|
systemd.services.${serviceName}.unitConfig.RequiresMountsFor = [
|
|
cfg.configDir
|
|
cfg.dataDir
|
|
];
|
|
|
|
services = {
|
|
postgresql = lib.mkIf cfg.configureDb {
|
|
enable = true;
|
|
ensureDatabases = [ name ];
|
|
ensureUsers = [
|
|
{
|
|
name = name;
|
|
ensureDBOwnership = true;
|
|
}
|
|
];
|
|
};
|
|
redis.servers.${name} = lib.mkIf cfg.redis.enable {
|
|
inherit (cfg.redis) enable port;
|
|
};
|
|
};
|
|
};
|
|
in
|
|
{ lib, ... }:
|
|
{
|
|
imports = [
|
|
{ config = lib.mkIf cfg.enable defaultConfig; }
|
|
{ config = lib.mkIf cfg.enable moduleConfig; }
|
|
];
|
|
|
|
options.${namespace}.${domain}.${name} = lib.mkOption {
|
|
type = lib.types.submodule {
|
|
options = {
|
|
enable = lib.mkEnableOption description;
|
|
|
|
port = mkOpt types.int 80 "Port for ${name} to listen on";
|
|
|
|
configDir = mkOpt types.str "/var/lib/${name}" "Path to the config directory";
|
|
|
|
dataDir = mkOpt types.str "/var/lib/${name}/data" "Path to the data directory";
|
|
|
|
createUser = mkBoolOpt false "Create a dedicated system user for this service";
|
|
|
|
configureDb = mkBoolOpt false "Manage a PostgreSQL database for this service";
|
|
|
|
environmentFile =
|
|
mkOpt (types.nullOr types.str) null
|
|
"Path to an environment file (EnvironmentFile=)";
|
|
|
|
puid = mkOpt types.str "911" "User ID for container-based services";
|
|
|
|
pgid = mkOpt types.str "100" "Group ID for container-based services";
|
|
|
|
timeZone = mkOpt types.str "UTC" "Timezone for container-based services";
|
|
|
|
listenAddress = mkOpt types.str "0.0.0.0" "Listen address";
|
|
|
|
openFirewall = mkBoolOpt true "Open firewall ports for this service";
|
|
|
|
redis = {
|
|
enable = lib.mkEnableOption "a dedicated Redis server for this service";
|
|
port = mkOpt types.int 6379 "Redis port for ${name}";
|
|
};
|
|
|
|
hashedPassword =
|
|
mkOpt (types.nullOr types.str) null
|
|
"Hashed password (e.g. for web-based authentication)";
|
|
|
|
extraEnvironment =
|
|
mkOpt (types.attrsOf types.str) { }
|
|
"Extra environment variables passed to the service";
|
|
|
|
reverseProxy = mkReverseProxyOpt name;
|
|
|
|
hostedService = {
|
|
enable =
|
|
mkOpt types.bool cfg.reverseProxy.enable
|
|
"Expose this service in Glance dashboard (auto-enabled when reverseProxy is on)";
|
|
title = mkOpt types.str name "Display title in Glance";
|
|
icon = mkOpt types.str "si:glance" "Icon identifier for Glance (e.g. si:actualbudget)";
|
|
group = mkOpt types.str "Services" "Glance group/category for this service";
|
|
url = mkOpt types.str (
|
|
if cfg.reverseProxy.enable then
|
|
"https://${cfg.reverseProxy.subdomain}.${cfg.reverseProxy.domain}"
|
|
else
|
|
"http://127.0.0.1:${toString cfg.port}"
|
|
) "Service URL for Glance (auto-derived from reverseProxy if enabled)";
|
|
basicAuth = mkOpt types.bool false "Require basic auth for this service in Glance";
|
|
};
|
|
}
|
|
// options;
|
|
};
|
|
default = { };
|
|
};
|
|
};
|
|
|
|
# Wraps mkModule for Podman/OCI container services. Generates all the
|
|
# standard mkModule options plus the container definition. The serviceName
|
|
# is set to "podman-<name>" automatically.
|
|
#
|
|
# Required args:
|
|
# config — the NixOS config attrset (pass through from the module args)
|
|
# name — service name (used for the container name and option path)
|
|
# image — OCI image reference string
|
|
# internalPort — port the container listens on internally
|
|
#
|
|
# Optional args:
|
|
# description — human-readable description (defaults to name)
|
|
# options — extra mkModule options attrset
|
|
# volumes — extra volume strings (in addition to none)
|
|
# environment — extra environment variables (merged with PUID/PGID/TZ)
|
|
# environmentFiles — list of paths to env-files (e.g. sops template paths)
|
|
# extraOptions — list of extra --opt strings passed to the container runtime
|
|
# devices — list of device mappings
|
|
# extraConfig — extra NixOS config merged into moduleConfig
|
|
mkContainerService =
|
|
{
|
|
config,
|
|
name,
|
|
image,
|
|
internalPort,
|
|
description ? name,
|
|
options ? { },
|
|
volumes ? [ ],
|
|
environment ? { },
|
|
environmentFiles ? [ ],
|
|
extraOptions ? [ ],
|
|
devices ? [ ],
|
|
extraConfig ? { },
|
|
}:
|
|
let
|
|
cfg = config.${namespace}.services.${name};
|
|
in
|
|
mkModule {
|
|
inherit
|
|
config
|
|
name
|
|
description
|
|
options
|
|
;
|
|
serviceName = "podman-${name}";
|
|
moduleConfig = lib.recursiveUpdate {
|
|
virtualisation.oci-containers.containers.${name} = {
|
|
autoStart = true;
|
|
inherit
|
|
image
|
|
volumes
|
|
environmentFiles
|
|
extraOptions
|
|
devices
|
|
;
|
|
ports = [ "${toString cfg.port}:${toString internalPort}" ];
|
|
environment = {
|
|
PUID = cfg.puid;
|
|
PGID = cfg.pgid;
|
|
TZ = cfg.timeZone;
|
|
}
|
|
// environment;
|
|
};
|
|
} extraConfig;
|
|
};
|
|
|
|
# Generates a sops secrets block + a sops template env-file in a single call.
|
|
#
|
|
# secrets — attrset of sops secret keys → extra attrs (e.g. owner/group).
|
|
# The sopsFile is set automatically to nas-secrets.yaml unless
|
|
# overridden per-secret via { sopsFile = ...; }.
|
|
# name — template file name, e.g. "glance.env"
|
|
# content — the template body string (use config.sops.placeholder."key")
|
|
# restartUnit — systemd unit to restart when the secret changes
|
|
# owner, group, mode — file ownership/permissions (defaults match NAS convention)
|
|
# sopsFile — default sops file for all secrets (can be overridden per-secret)
|
|
mkSopsEnvFile =
|
|
{
|
|
secrets,
|
|
name,
|
|
content,
|
|
restartUnit,
|
|
owner ? "nix-apps",
|
|
group ? "jallen-nas",
|
|
mode ? "660",
|
|
sopsFile ? lib.snowfall.fs.get-file "secrets/nas-secrets.yaml",
|
|
}:
|
|
{
|
|
sops.secrets = mapAttrs (_key: extra: { inherit sopsFile; } // extra) secrets;
|
|
|
|
sops.templates.${name} = {
|
|
inherit
|
|
mode
|
|
owner
|
|
group
|
|
content
|
|
;
|
|
restartUnits = [ restartUnit ];
|
|
};
|
|
};
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Home Manager module helper
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Create a Home Manager module with a standard enable option and optional
|
|
# extra options, gating all config behind `cfg.enable`.
|
|
#
|
|
# domain — option namespace domain, e.g. "programs" or "desktop"
|
|
# name — module name, e.g. "btop"
|
|
# description — text for mkEnableOption (defaults to name)
|
|
# options — attrset of extra options merged into the submodule
|
|
# config — the NixOS/HM config attrset passed through from module args
|
|
# moduleConfig — the Home Manager config body (already gated behind cfg.enable)
|
|
mkHomeModule =
|
|
{
|
|
config,
|
|
domain,
|
|
name,
|
|
description ? name,
|
|
options ? { },
|
|
moduleConfig,
|
|
}:
|
|
let
|
|
cfg = config.${namespace}.${domain}.${name};
|
|
in
|
|
{ lib, ... }:
|
|
{
|
|
options.${namespace}.${domain}.${name} = lib.mkOption {
|
|
type = lib.types.submodule {
|
|
options = {
|
|
enable = lib.mkEnableOption description;
|
|
}
|
|
// options;
|
|
};
|
|
default = { };
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable moduleConfig;
|
|
};
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Option creation helpers
|
|
# ---------------------------------------------------------------------------
|
|
# Option creation helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
mkOpt =
|
|
type: default: description:
|
|
mkOption { inherit type default description; };
|
|
|
|
mkOpt' = type: default: mkOpt type default "";
|
|
|
|
mkBoolOpt = mkOpt types.bool;
|
|
|
|
mkBoolOpt' = mkOpt' types.bool;
|
|
|
|
mkReverseProxyOpt = name: {
|
|
enable = mkBoolOpt false "Enable Caddy reverse proxy for this service";
|
|
|
|
subdomain = mkOpt types.str name "Subdomain for the service (default: service name)";
|
|
|
|
domain = mkOpt types.str "mjallen.dev" "Base domain for the reverse proxy";
|
|
|
|
upstreamUrl =
|
|
mkOpt (types.nullOr types.str) null
|
|
"Override upstream URL (e.g. for a service on a different host). Defaults to http://127.0.0.1:<port>.";
|
|
|
|
extraCaddyConfig = mkOpt types.lines "" "Extra Caddyfile directives inside this virtual host block";
|
|
};
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Convenience shorthands
|
|
# ---------------------------------------------------------------------------
|
|
|
|
enabled = {
|
|
enable = true;
|
|
};
|
|
disabled = {
|
|
enable = false;
|
|
};
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# String utilities
|
|
# ---------------------------------------------------------------------------
|
|
|
|
capitalize =
|
|
s:
|
|
let
|
|
len = stringLength s;
|
|
in
|
|
if len == 0 then "" else (toUpper (substring 0 1 s)) + (substring 1 len s);
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Boolean utilities
|
|
# ---------------------------------------------------------------------------
|
|
|
|
boolToNum = bool: if bool then 1 else 0;
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Attribute manipulation utilities
|
|
# ---------------------------------------------------------------------------
|
|
|
|
default-attrs = mapAttrs (_key: mkDefault);
|
|
|
|
force-attrs = mapAttrs (_key: mkForce);
|
|
|
|
nested-default-attrs = mapAttrs (_key: default-attrs);
|
|
|
|
nested-force-attrs = mapAttrs (_key: force-attrs);
|
|
}
|