236 lines
7.2 KiB
Nix
236 lines
7.2 KiB
Nix
{
|
|
inputs,
|
|
lib,
|
|
namespace,
|
|
}:
|
|
let
|
|
inherit (inputs.nixpkgs.lib)
|
|
mapAttrs
|
|
mkOption
|
|
types
|
|
toUpper
|
|
substring
|
|
stringLength
|
|
mkDefault
|
|
mkForce
|
|
;
|
|
in
|
|
rec {
|
|
|
|
# Conditionally enable modules based on system
|
|
enableForSystem =
|
|
system: modules:
|
|
builtins.filter (
|
|
mod: mod.systems or [ ] == [ ] || builtins.elem system (mod.systems or [ ])
|
|
) modules;
|
|
|
|
# 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}
|
|
}
|
|
'';
|
|
};
|
|
|
|
# Open firewall
|
|
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} = { };
|
|
};
|
|
|
|
# Ensure the service waits for the filesystem that hosts configDir and
|
|
# dataDir to be mounted before starting. RequiresMountsFor is the
|
|
# idiomatic systemd way to express this: if the paths live on the root
|
|
# filesystem the directive is silently ignored, so it is safe on every
|
|
# host — not just the NAS.
|
|
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 {
|
|
enable = true;
|
|
port = cfg.redis.port;
|
|
};
|
|
};
|
|
};
|
|
in
|
|
{ lib, ... }:
|
|
{
|
|
imports = [
|
|
# defaultConfig and moduleConfig are kept as separate inline modules so
|
|
# the NixOS module system handles all merging (mkIf, mkForce, mkMerge,
|
|
# etc.) correctly, rather than merging raw attrsets with // or
|
|
# recursiveUpdate which can silently clobber mkIf wrappers.
|
|
{ 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;
|
|
}
|
|
// options;
|
|
};
|
|
default = { };
|
|
};
|
|
};
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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);
|
|
}
|