{ config, lib, ... }: let cfg = config.services.ess-helm; inherit (lib) literalExpression mkDefault mkEnableOption mkIf mkOption recursiveUpdate types ; chartModule = { defaultRepo, defaultName, defaultVersion, defaultHash, ... }: types.submodule { options = { repo = mkOption { type = types.str; default = defaultRepo; description = "Helm chart repository URL. For OCI registries this must include the chart name (e.g. `oci://ghcr.io/org/repo/chart`)."; }; name = mkOption { type = types.str; default = defaultName; description = "Chart name. For HTTP repositories appended to the pull command. For OCI repositories only used to name the store derivation."; }; version = mkOption { type = types.str; default = defaultVersion; description = "Chart version to deploy."; example = "26.5.0"; }; hash = mkOption { type = types.str; default = defaultHash; description = "SRI hash of the chart `.tgz` archive."; example = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; }; extraValues = mkOption { type = types.attrs; default = { }; description = "Additional Helm values merged on top of the module-generated ones"; example = literalExpression '' { synapse.additional."user-config.yaml".config = "max_upload_size: 100M"; } ''; }; }; }; serviceModule = { defaultSubdomain }: types.submodule ( { config, ... }: { options = { subdomain = mkOption { type = types.str; default = defaultSubdomain; description = "Subdomain for this service. Full hostname is `.`, or just `` when empty (Synapse)."; }; forceSSL = mkOption { type = types.bool; default = true; description = "Redirect plain-HTTP requests to HTTPS."; }; enableACME = mkOption { type = types.bool; description = "Obtain a Let's Encrypt certificate for this vhost. Defaults to the value of `forceSSL`."; }; }; config.enableACME = mkDefault config.forceSSL; } ); hostFor = svc: if svc.subdomain == "" then cfg.serverName else "${svc.subdomain}.${cfg.serverName}"; vhostFor = svc: { inherit (svc) forceSSL enableACME; locations."/" = { proxyPass = "http://127.0.0.1:${toString cfg.ingressNginxPort}"; proxyWebsockets = true; extraConfig = '' proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 86400s; proxy_send_timeout 86400s; proxy_buffering off; client_max_body_size 50M; ''; }; }; in { options.services.ess-helm = { enable = mkEnableOption "Element Server Suite Community"; serverName = mkOption { type = types.str; description = "Matrix server name — the domain part of every Matrix ID (`@user:`). Must point at this host and **cannot be changed** after the first deployment without wiping the database."; example = "example.com"; }; openFirewall = mkOption { type = types.bool; default = false; description = "Open the Matrix RTC WebRTC ports in the firewall (TCP 30001, UDP 30002)."; }; configureNginx = mkOption { type = types.bool; default = true; description = "Configure nginx as a TLS-terminating reverse proxy for all ESS services, proxying to ingress-nginx on `ingressNginxPort`."; }; ingressNginxPort = mkOption { type = types.port; default = 30080; description = "NodePort for ingress-nginx plain HTTP. Must be in the Kubernetes NodePort range (30000–32767)."; }; ingressNginxTlsPort = mkOption { type = types.port; default = 30443; description = "NodePort reserved for ingress-nginx HTTPS (unused at runtime but required by the chart schema). Must be in the Kubernetes NodePort range (30000–32767)."; }; ipFamily = mkOption { type = types.enum [ "ipv4" "ipv6" "dual" ]; default = "ipv4"; description = "IP family for the ESS cluster networking."; }; synapse = mkOption { type = serviceModule { defaultSubdomain = ""; }; default = { }; description = "Synapse homeserver (served at the root serverName domain)."; }; matrixAuthenticationService = mkOption { type = serviceModule { defaultSubdomain = "auth"; }; default = { }; description = "Matrix Authentication Service (MAS)."; }; elementWeb = mkOption { type = serviceModule { defaultSubdomain = "chat"; }; default = { }; description = "Element Web client."; }; elementAdmin = mkOption { type = serviceModule { defaultSubdomain = "admin"; }; default = { }; description = "Element Admin console."; }; matrixRTC = mkOption { type = serviceModule { defaultSubdomain = "mrtc"; }; default = { }; description = "Matrix RTC SFU signalling endpoint."; }; ess = mkOption { type = chartModule { name = "ess"; defaultRepo = "oci://ghcr.io/element-hq/ess-helm/matrix-stack"; defaultName = "matrix-stack"; defaultVersion = "26.5.0"; defaultHash = "sha256-2YfDk29c8MoEa7rJXnHiKgKVqL7I0mmIcbtqgh2tunU="; }; default = { }; description = "ESS Community Helm chart pin and extra values."; }; ingressNginx = mkOption { type = chartModule { name = "ingressNginx"; defaultRepo = "https://kubernetes.github.io/ingress-nginx"; defaultName = "ingress-nginx"; defaultVersion = "4.15.1"; defaultHash = "sha256-Pv8L0YFR1uaxxEFGNBBXFEPdoax4KSyxiTRmKN54Tww="; }; default = { }; description = "ingress-nginx Helm chart pin and extra values."; }; }; config = mkIf cfg.enable { services.k3s = { enable = true; role = "server"; disable = [ "traefik" ]; # use ingress-nginx autoDeployCharts = { ingress-nginx = recursiveUpdate { inherit (cfg.ingressNginx) repo name version hash ; targetNamespace = "ingress-nginx"; createNamespace = true; values = { controller = { ingressClassResource.default = true; service = { type = "NodePort"; nodePorts = { http = cfg.ingressNginxPort; https = cfg.ingressNginxTlsPort; }; }; config = { use-forwarded-headers = "true"; compute-full-forwarded-for = "true"; proxy-read-timeout = "86400"; proxy-send-timeout = "86400"; proxy-body-size = "50m"; }; }; }; } { values = cfg.ingressNginx.extraValues; }; ess = recursiveUpdate { inherit (cfg.ess) repo name version hash ; targetNamespace = "ess"; createNamespace = true; values = { inherit (cfg) serverName; ingress = { className = "nginx"; tlsEnabled = false; }; synapse.ingress.host = hostFor cfg.synapse; matrixAuthenticationService.ingress.host = hostFor cfg.matrixAuthenticationService; elementWeb.ingress.host = hostFor cfg.elementWeb; elementAdmin.ingress.host = hostFor cfg.elementAdmin; matrixRTC.ingress.host = hostFor cfg.matrixRTC; networking.ipFamily = cfg.ipFamily; }; } { values = cfg.ess.extraValues; }; }; }; services.nginx = mkIf cfg.configureNginx { enable = true; virtualHosts = { "${hostFor cfg.synapse}" = vhostFor cfg.synapse; "${hostFor cfg.matrixAuthenticationService}" = vhostFor cfg.matrixAuthenticationService; "${hostFor cfg.elementWeb}" = vhostFor cfg.elementWeb; "${hostFor cfg.elementAdmin}" = vhostFor cfg.elementAdmin; "${hostFor cfg.matrixRTC}" = vhostFor cfg.matrixRTC; }; }; networking.firewall = mkIf cfg.openFirewall { allowedTCPPorts = [ 30001 ]; allowedUDPPorts = [ 30002 ]; }; environment.variables.KUBECONFIG = "/etc/rancher/k3s/k3s.yaml"; }; }