ess-helm-nixos/modules/nixos/ess-helm/default.nix

301 lines
8.7 KiB
Nix

{
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 `<subdomain>.<serverName>`, or just `<serverName>` 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:<serverName>`). 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";
};
}