diff --git a/AGENTS.md b/AGENTS.md index 1bd1f83..7abd7f3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -103,3 +103,6 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla Keep `git.alexion.dev:2022` working by proxying that TCP stream through the reverse-proxy guest to Gitea. - Remote deploy helpers that run target-side `sudo` over SSH need a TTY. Use `ssh -t` unless the target command is guaranteed not to prompt for sudo credentials. +- A NixOS module function argument default such as `registry ? config.registry` is not a safe fallback for an unprovided module argument. + The module system still queries `_module.args.registry` and fails when it is absent. + When an optional special-argument fallback is intentionally needed, bind the module arguments through `@args` and test `args ? ` inside the module body. diff --git a/flake.nix b/flake.nix index b3ddcf1..7df05a1 100644 --- a/flake.nix +++ b/flake.nix @@ -193,20 +193,33 @@ reverse-proxy-basic-route = let + routeHost = lib.nixosSystem { + system = "x86_64-linux"; + specialArgs.my = self.lib; + modules = [ + self.lib.registry.module + { + registry.address.test.budget = "10.23.20.42"; + registry.route.test.budget = { + domain = "budget.alexion.dev"; + port = 5006; + }; + system.stateVersion = "26.11"; + } + ]; + }; + registry = self.lib.registry.collect { hosts.test = routeHost; }; testConfig = (lib.nixosSystem { system = "x86_64-linux"; - specialArgs.my = self.lib; + specialArgs = { + my = self.lib; + inherit registry; + }; modules = [ ./modules/reverse-proxy.nix { - modules.reverse-proxy = { - enable = true; - routes."budget.alexion.dev" = { - path = "/"; - backend = "http://10.23.20.42:5006"; - }; - }; + modules.reverse-proxy.enable = true; } ]; }).config; @@ -224,21 +237,22 @@ reverse-proxy-actualbudget-guest-route = let testConfig = self.nixosConfigurations.pikachu.config; - route = testConfig.modules.reverse-proxy.routes.actualbudget; + route = testConfig.registry.route.pikachu.actualbudget; vhost = testConfig.containers.reverse-proxy.config.services.nginx.virtualHosts."budget.alexion.dev"; in pkgs.runCommand "reverse-proxy-actualbudget-guest-route-check" { } '' ${ lib.optionalString ( !( - route.host == "budget.alexion.dev" - && route.backend == null - && route.endpoint == "pikachu-actualbudget" + route.domain == "budget.alexion.dev" && route.port == 5006 - && route.tls - && route.aliases == [ ] - && route.path == "/" - && !route.websockets + && !(route ? backend) + && !(route ? path) + && !(route ? scheme) + && !(route ? aliases) + && !(route ? tls) + && !(route ? websockets) + && !(route ? enable) && vhost.locations."/".proxyPass == "http://10.23.20.42:5006" ) ) "exit 1" @@ -313,7 +327,6 @@ inputs.sops-nix.nixosModules.sops self.lib.registry.module ./modules/network.nix - ./modules/reverse-proxy.nix (self.lib.guest { name = "copyparty"; }) { networking.hostName = "pikachu"; @@ -361,34 +374,46 @@ system = "x86_64-linux"; specialArgs.my = self.lib; modules = [ - ./modules/reverse-proxy.nix + self.lib.registry.module { - modules.reverse-proxy.routes.app = { - host = "worker.alexion.dev"; - backend = "http://10.23.20.126:3923"; + registry.address.routeHost.app = "10.23.20.126"; + registry.route.routeHost.app = { + domain = "worker.alexion.dev"; + port = 3923; }; + system.stateVersion = "26.11"; } ]; }; - endpoints = self.lib.networkEndpoints.collectFleetEndpoints { - pikachu = self.nixosConfigurations.pikachu; + registry = self.lib.registry.collect { + hosts = { + inherit routeHost; + pikachu = self.nixosConfigurations.pikachu; + }; }; - fleetRoutes = self.lib.reverseProxyRoutes.collectFleetRoutes { - inherit routeHost; - pikachu = self.nixosConfigurations.pikachu; - } endpoints; testConfig = - (self.nixosConfigurations.pikachu.extendModules { - specialArgs.reverseProxyFleetRoutes = fleetRoutes; + (lib.nixosSystem { + system = "x86_64-linux"; + specialArgs = { + my = self.lib; + inherit registry; + }; + modules = [ + ./modules/reverse-proxy.nix + { + modules.reverse-proxy.enable = true; + } + ]; }).config; - vhosts = testConfig.containers.reverse-proxy.config.services.nginx.virtualHosts; + vhosts = testConfig.services.nginx.virtualHosts; in pkgs.runCommand "reverse-proxy-fleet-routes-check" { } '' ${ lib.optionalString ( !( - fleetRoutes ? routeHost-app - && fleetRoutes ? pikachu-actualbudget + registry.route ? routeHost + && registry.route.routeHost ? app + && registry.route.pikachu ? actualbudget && vhosts."worker.alexion.dev".locations."/".proxyPass == "http://10.23.20.126:3923" && vhosts."budget.alexion.dev".locations."/".proxyPass == "http://10.23.20.42:5006" && vhosts."files.alexion.dev".locations."/".proxyPass == "http://10.23.20.126:3923" @@ -398,9 +423,9 @@ touch $out ''; - reverse-proxy-endpoint-backends = + reverse-proxy-address-backends = let - endpointHost = + workerHost = lib.nixosSystem { system = "x86_64-linux"; specialArgs.my = self.lib; @@ -408,22 +433,11 @@ self.lib.registry.module { registry.address.worker.self = "10.23.50.146"; - system.stateVersion = "26.11"; - } - ]; - }; - routeHost = - lib.nixosSystem { - system = "x86_64-linux"; - specialArgs.my = self.lib; - modules = [ - ./modules/reverse-proxy.nix - { - modules.reverse-proxy.routes.app = { - host = "worker.alexion.dev"; - endpoint = "worker"; + registry.route.worker.self = { + domain = "worker.alexion.dev"; port = 8080; }; + system.stateVersion = "26.11"; } ]; }; @@ -435,7 +449,6 @@ inputs.sops-nix.nixosModules.sops self.lib.registry.module ./modules/network.nix - ./modules/reverse-proxy.nix (self.lib.guest { name = "files"; }) { networking.hostName = "pikachu"; @@ -446,44 +459,47 @@ address = "10.23.20.126"; reverseProxy = { enable = true; - host = "files.alexion.dev"; - port = 3923; + route = { + domain = "files.alexion.dev"; + port = 3923; + }; }; }; system.stateVersion = "26.11"; } ]; }; - endpoints = self.lib.networkEndpoints.collectFleetEndpoints { - worker = endpointHost; - pikachu = guestRouteHost; + registry = self.lib.registry.collect { + hosts = { + worker = workerHost; + pikachu = guestRouteHost; + }; }; - fleetRoutes = self.lib.reverseProxyRoutes.collectFleetRoutes { - inherit routeHost; - pikachu = guestRouteHost; - } endpoints; testConfig = (lib.nixosSystem { system = "x86_64-linux"; - specialArgs.my = self.lib; + specialArgs = { + my = self.lib; + inherit registry; + }; modules = [ ./modules/reverse-proxy.nix { - modules.reverse-proxy = { - enable = true; - routes = fleetRoutes; - }; + modules.reverse-proxy.enable = true; } ]; }).config; vhosts = testConfig.services.nginx.virtualHosts; + workerCompositeEntry = registry.join "worker" "self"; in - pkgs.runCommand "reverse-proxy-endpoint-backends-check" { } '' + pkgs.runCommand "reverse-proxy-address-backends-check" { } '' ${ lib.optionalString ( !( - vhosts."worker.alexion.dev".locations."/".proxyPass == "http://10.23.50.146:8080" - && fleetRoutes."pikachu-files".endpoint == "pikachu-files" + workerCompositeEntry.address == "10.23.50.146" + && workerCompositeEntry.route.domain == "worker.alexion.dev" + && vhosts."worker.alexion.dev".locations."/".proxyPass == "http://10.23.50.146:8080" + && registry.route.pikachu.files.port == 3923 && vhosts."files.alexion.dev".locations."/".proxyPass == "http://10.23.20.126:3923" ) ) "exit 1" @@ -491,34 +507,95 @@ touch $out ''; - reverse-proxy-secondary-path-requires-root = + reverse-proxy-route-requires-address = let - testConfig = - (lib.nixosSystem { - system = "x86_64-linux"; - specialArgs.my = self.lib; - modules = [ - ./modules/reverse-proxy.nix - { - modules.reverse-proxy = { - enable = true; - routes.raichu-files = { - host = "files.alexion.dev"; - path = "/raichu"; - backend = "http://10.23.20.10:3923"; - }; - }; - } - ]; - }).config; - hasFailedRootAssertion = lib.any ( - assertion: - assertion.assertion == false - && assertion.message == "modules.reverse-proxy.routes for files.alexion.dev must include a primary / route." - ) testConfig.assertions; + routeHost = lib.nixosSystem { + system = "x86_64-linux"; + specialArgs.my = self.lib; + modules = [ + self.lib.registry.module + { + registry.route.raichu.files = { + domain = "files.alexion.dev"; + port = 3923; + }; + system.stateVersion = "26.11"; + } + ]; + }; + registry = self.lib.registry.collect { hosts.raichu = routeHost; }; + testConfig = lib.nixosSystem { + system = "x86_64-linux"; + specialArgs = { + my = self.lib; + inherit registry; + }; + modules = [ + ./modules/reverse-proxy.nix + { + modules.reverse-proxy.enable = true; + system.stateVersion = "26.11"; + } + ]; + }; + result = builtins.tryEval (builtins.deepSeq testConfig.config.services.nginx.virtualHosts true); in - pkgs.runCommand "reverse-proxy-secondary-path-requires-root-check" { } '' - ${lib.optionalString (!hasFailedRootAssertion) "exit 1"} + pkgs.runCommand "reverse-proxy-route-requires-address-check" { } '' + ${lib.optionalString result.success "exit 1"} + touch $out + ''; + + reverse-proxy-duplicate-domain-fails = + let + pikachu = lib.nixosSystem { + system = "x86_64-linux"; + specialArgs.my = self.lib; + modules = [ + self.lib.registry.module + { + registry.address.pikachu.copyparty = "10.23.20.126"; + registry.route.pikachu.copyparty = { + domain = "files.alexion.dev"; + port = 3923; + }; + system.stateVersion = "26.11"; + } + ]; + }; + raichu = lib.nixosSystem { + system = "x86_64-linux"; + specialArgs.my = self.lib; + modules = [ + self.lib.registry.module + { + registry.address.raichu.files = "10.23.20.10"; + registry.route.raichu.files = { + domain = "files.alexion.dev"; + port = 3923; + }; + system.stateVersion = "26.11"; + } + ]; + }; + registry = self.lib.registry.collect { hosts = { inherit pikachu raichu; }; }; + testConfig = lib.nixosSystem { + system = "x86_64-linux"; + specialArgs = { + my = self.lib; + inherit registry; + }; + modules = [ + ./modules/reverse-proxy.nix + { + modules.reverse-proxy.enable = true; + system.stateVersion = "26.11"; + } + ]; + }; + result = builtins.tryEval (builtins.deepSeq testConfig.config.services.nginx.virtualHosts true); + in + pkgs.runCommand "reverse-proxy-duplicate-domain-fails-check" { } '' + ${lib.optionalString result.success "exit 1"} touch $out ''; }; diff --git a/guests/reverse-proxy.nix b/guests/reverse-proxy.nix index 03d7cca..ebc8006 100644 --- a/guests/reverse-proxy.nix +++ b/guests/reverse-proxy.nix @@ -2,40 +2,59 @@ config, lib, my, - reverseProxyFleetRoutes ? config.modules.reverse-proxy.routes, ... }: let giteaSshBackend = "10.23.20.45:2022"; - legacyBackend = "http://10.23.20.32"; - legacyHosts = [ - "games.alexion.dev" - "git.alexion.dev" - "music.alexion.dev" - "photos.alexion.dev" - "plex.alexion.dev" - "servers.alexion.dev" - "todo.alexion.dev" - "wings.alexion.dev" - ]; - legacyRoutes = lib.listToAttrs ( - map (host: { - name = "legacy-${lib.replaceStrings [ "." ] [ "-" ] host}"; - value = { - inherit host; - backend = legacyBackend; - }; - }) legacyHosts - ); in { imports = [ (my.guest { name = "reverse-proxy"; interior = { - modules.reverse-proxy = { - enable = true; - routes = reverseProxyFleetRoutes // legacyRoutes; + modules.reverse-proxy.enable = true; + + services.nginx.virtualHosts = { + "games.alexion.dev" = { + forceSSL = true; + enableACME = true; + locations."/".proxyPass = "http://10.23.20.32"; + }; + "git.alexion.dev" = { + forceSSL = true; + enableACME = true; + locations."/".proxyPass = "http://10.23.20.32"; + }; + "music.alexion.dev" = { + forceSSL = true; + enableACME = true; + locations."/".proxyPass = "http://10.23.20.32"; + }; + "photos.alexion.dev" = { + forceSSL = true; + enableACME = true; + locations."/".proxyPass = "http://10.23.20.32"; + }; + "plex.alexion.dev" = { + forceSSL = true; + enableACME = true; + locations."/".proxyPass = "http://10.23.20.32"; + }; + "servers.alexion.dev" = { + forceSSL = true; + enableACME = true; + locations."/".proxyPass = "http://10.23.20.32"; + }; + "todo.alexion.dev" = { + forceSSL = true; + enableACME = true; + locations."/".proxyPass = "http://10.23.20.32"; + }; + "wings.alexion.dev" = { + forceSSL = true; + enableACME = true; + locations."/".proxyPass = "http://10.23.20.32"; + }; }; networking.firewall.allowedTCPPorts = [ 2022 ]; diff --git a/hosts/pikachu/default.nix b/hosts/pikachu/default.nix index 643ad6d..5ee167e 100644 --- a/hosts/pikachu/default.nix +++ b/hosts/pikachu/default.nix @@ -87,8 +87,10 @@ address = "10.23.20.42"; reverseProxy = { enable = true; - host = "budget.alexion.dev"; - port = 5006; + route = { + domain = "budget.alexion.dev"; + port = 5006; + }; }; }; @@ -107,8 +109,10 @@ address = "10.23.20.126"; reverseProxy = { enable = true; - host = "files.alexion.dev"; - port = 3923; + route = { + domain = "files.alexion.dev"; + port = 3923; + }; trustedSources = [ "${registry.address.${config.networking.hostName}."reverse-proxy"}/32" ]; }; volumes = { diff --git a/lib/default.nix b/lib/default.nix index ba3a9f5..48bff10 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -16,7 +16,6 @@ let serviceIdentityRegistry = import ./service-identities.nix { inherit lib; }; registry = import ./registry.nix { inherit lib self; }; networkEndpoints = import ./network-endpoints.nix { inherit lib; }; - reverseProxyRoutes = import ./reverse-proxy-routes.nix { inherit lib; }; collectNixFiles = dir: @@ -35,16 +34,11 @@ let ) (builtins.readDir dir) ); - specialArgs = - let - networkFleetEndpoints = self.lib.networkEndpoints.collectFleetEndpoints self.nixosConfigurations; - in - { - inherit inputs networkFleetEndpoints self; - my = self.lib; - registry = self.lib.registry.collect { hosts = self.nixosConfigurations; }; - reverseProxyFleetRoutes = self.lib.reverseProxyRoutes.collectFleetRoutes self.nixosConfigurations networkFleetEndpoints; - }; + specialArgs = { + inherit inputs self; + my = self.lib; + registry = self.lib.registry.collect { hosts = self.nixosConfigurations; }; + }; bridgeName = id: "br-vlan${toString id}"; @@ -54,7 +48,6 @@ let self specialArgs bridgeName - reverseProxyRoutes ; }; @@ -95,7 +88,6 @@ in bridgeName registry networkEndpoints - reverseProxyRoutes ; inherit (guests) deriveMac guest; diff --git a/lib/guests.nix b/lib/guests.nix index 1750756..1c1a0fe 100644 --- a/lib/guests.nix +++ b/lib/guests.nix @@ -3,7 +3,6 @@ self, specialArgs, bridgeName, - reverseProxyRoutes, }: let inherit (lib) escapeShellArg mapAttrsToList; @@ -130,9 +129,13 @@ let Left null, this guest contributes no address to the address registry. ''; }; - reverseProxy = reverseProxyRoutes.mkRouteOptions { - subject = "guest route"; - enableDefault = false; + reverseProxy = { + enable = lib.mkEnableOption "a reverse-proxy route for this guest"; + + route = lib.mkOption { + type = self.lib.registry.definitions.route.type; + description = "Route registry entry published for this guest."; + }; }; mounts = lib.mkOption { type = lib.types.attrsOf ( @@ -279,14 +282,7 @@ let registry.address.${config.networking.hostName}.${machineName} = lib.mkIf (cfg.address != null) cfg.address; - modules.reverse-proxy.routes.${machineName} = lib.mkIf cfg.reverseProxy.enable ( - reverseProxyRoutes.routeFields ( - cfg.reverseProxy - // lib.optionalAttrs (cfg.reverseProxy.backend == null && cfg.reverseProxy.endpoint == null) { - endpoint = "${config.networking.hostName}-${machineName}"; - } - ) - ); + registry.route.${config.networking.hostName}.${machineName} = lib.mkIf cfg.reverseProxy.enable cfg.reverseProxy.route; assertions = [ { diff --git a/lib/registry.nix b/lib/registry.nix index d43d6f4..c0724a4 100644 --- a/lib/registry.nix +++ b/lib/registry.nix @@ -28,6 +28,23 @@ let in nameValuePair (registryName name) (import path { inherit lib; }) ) (filterAttrs (name: type: type == "regular" && hasSuffix ".nix" name) (builtins.readDir (self + "/registry"))); + join = registry: host: entry: + filterAttrs (_name: value: value != null) ( + mapAttrs (registryName: _registry: (registry.${registryName}.${host} or { }).${entry} or null) definitions + ); + + collectData = + { hosts }: + mapAttrs ( + registryName: _registry: + mapAttrs (hostName: host: host.config.registry.${registryName}.${hostName} or { }) hosts + ) definitions; + + withOperations = registry: + registry + // { + join = host: entry: join registry host entry; + }; in { inherit definitions; @@ -36,10 +53,5 @@ in options.registry = mapAttrs (_name: registry: registryOption registry) definitions; }; - collect = - { hosts }: - mapAttrs ( - registryName: _registry: - mapAttrs (hostName: host: host.config.registry.${registryName}.${hostName} or { }) hosts - ) definitions; + collect = args: withOperations (collectData args); } diff --git a/lib/reverse-proxy-routes.nix b/lib/reverse-proxy-routes.nix deleted file mode 100644 index 4a40a7d..0000000 --- a/lib/reverse-proxy-routes.nix +++ /dev/null @@ -1,129 +0,0 @@ -{ lib }: -let - mkRouteOptions = - { - name ? null, - subject ? "route", - enableDefault ? true, - }: - { - enable = lib.mkOption { - type = lib.types.bool; - default = enableDefault; - description = "Whether this reverse-proxy ${subject} is active."; - }; - - host = lib.mkOption ( - { - type = lib.types.str; - description = "Primary hostname served by this reverse-proxy ${subject}."; - } - // lib.optionalAttrs (name != null) { - default = name; - defaultText = lib.literalMD "the route attribute name"; - } - ); - - aliases = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - description = "Additional hostnames served by this reverse-proxy ${subject}."; - }; - - path = lib.mkOption { - type = lib.types.str; - default = "/"; - description = "URL path prefix proxied by this reverse-proxy ${subject}."; - }; - - backend = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - description = "Full HTTP backend URL that receives proxied traffic."; - }; - - endpoint = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - description = "Fleet network endpoint key used to derive the backend URL."; - }; - - port = lib.mkOption { - type = lib.types.nullOr (lib.types.ints.between 1 65535); - default = null; - example = 3923; - description = "Backend TCP port used with the fleet network endpoint."; - }; - - scheme = lib.mkOption { - type = lib.types.str; - default = "http"; - description = "Backend URL scheme used with the fleet network endpoint."; - }; - - tls = lib.mkOption { - type = lib.types.bool; - default = true; - description = "Whether to serve this reverse-proxy ${subject} with ACME-backed TLS."; - }; - - websockets = lib.mkOption { - type = lib.types.bool; - default = false; - description = "Whether to enable websocket proxy headers for this reverse-proxy ${subject}."; - }; - }; - - routeFields = route: { - inherit (route) - host - aliases - path - backend - endpoint - port - scheme - tls - websockets - ; - }; - - endpointBackend = endpoints: route: - let - endpoint = endpoints.${route.endpoint} or ( - throw "Unknown reverse-proxy endpoint `${route.endpoint}`. Known endpoints: ${lib.concatStringsSep ", " (lib.attrNames endpoints)}" - ); - in - "${route.scheme}://${endpoint.address}:${toString route.port}"; - - resolvedRouteFields = endpoints: route: - let - routeWithBackend = - if route.backend != null then - route - else if route.endpoint != null && route.port != null then - route // { backend = endpointBackend endpoints route; } - else - throw "Reverse-proxy route `${route.host}${route.path}` needs either backend or endpoint plus port."; - in - routeFields routeWithBackend; - - collectFleetRoutes = - hosts: endpoints: - lib.concatMapAttrs ( - hostName: host: - lib.mapAttrs' ( - routeName: route: lib.nameValuePair "${hostName}-${routeName}" (resolvedRouteFields endpoints route) - ) host.config.modules.reverse-proxy.routes - ) hosts; -in -{ - inherit collectFleetRoutes mkRouteOptions resolvedRouteFields routeFields; - - routeType = lib.types.submodule ( - { name, ... }: - { - options = mkRouteOptions { inherit name; }; - } - ); -} diff --git a/modules/reverse-proxy.nix b/modules/reverse-proxy.nix index aaae1d9..edd07ef 100644 --- a/modules/reverse-proxy.nix +++ b/modules/reverse-proxy.nix @@ -1,73 +1,71 @@ -{ config, lib, my, ... }: +{ + config, + lib, + registry, + ... +}: let cfg = config.modules.reverse-proxy; - inherit (my) reverseProxyRoutes; - enabledRoutes = lib.filterAttrs (_: route: route.enable) cfg.routes; - routesByHost = lib.groupBy (route: route.host) (lib.attrValues enabledRoutes); + mkRegistryPath = registryName: host: entry: "registry.${registryName}.${host}.${entry}"; - rootRoutesByHost = lib.mapAttrs ( - host: routes: + mkBackend = address: route: "http://${address}:${toString route.port}"; + + mkVirtualHost = host: entry: let - roots = builtins.filter (route: route.path == "/") routes; + compositeEntry = registry.join host entry; + route = compositeEntry.route; + address = compositeEntry.address or (throw "no matching entry in registry.address for ${mkRegistryPath "route" host entry}"); in - if roots == [ ] then null else builtins.head roots - ) routesByHost; - - hasDuplicatePaths = routes: - let - paths = map (route: route.path) routes; - in - lib.length paths != lib.length (lib.unique paths); - - hasUnresolvedBackend = route: route.backend == null; - - mkLocation = route: { - proxyPass = route.backend; - proxyWebsockets = route.websockets; - }; - - mkVirtualHost = host: routes: - let - rootRoute = rootRoutesByHost.${host}; - in - lib.mkIf (rootRoute != null) { - forceSSL = rootRoute.tls; - enableACME = rootRoute.tls; - serverAliases = rootRoute.aliases; - locations = lib.listToAttrs ( - map (route: lib.nameValuePair route.path (mkLocation route)) ( - builtins.filter (route: route.backend != null) routes - ) - ); + { + inherit host entry; + name = route.domain; + value = { + forceSSL = true; + enableACME = true; + locations."/".proxyPass = mkBackend address route; + }; }; + + generatedVirtualHostList = lib.concatLists ( + lib.mapAttrsToList ( + host: routes: + lib.mapAttrsToList (entry: _route: mkVirtualHost host entry) routes + ) registry.route + ); + + virtualHostsByDomain = lib.foldl' ( + acc: virtualHost: + acc + // { + ${virtualHost.name} = (acc.${virtualHost.name} or [ ]) ++ [ virtualHost ]; + } + ) { } generatedVirtualHostList; + + duplicateDomainMessages = lib.concatLists ( + lib.mapAttrsToList ( + domain: virtualHosts: + if lib.length virtualHosts > 1 then + [ + "duplicate route domain ${domain} in ${lib.concatMapStringsSep " and " (virtualHost: mkRegistryPath "route" virtualHost.host virtualHost.entry) virtualHosts}" + ] + else + [ ] + ) virtualHostsByDomain + ); + + generatedVirtualHosts = + if duplicateDomainMessages != [ ] then + throw (builtins.head duplicateDomainMessages) + else + lib.listToAttrs generatedVirtualHostList; in { options.modules.reverse-proxy = { - enable = lib.mkEnableOption "the Nginx reverse proxy route realizer"; - - routes = lib.mkOption { - type = lib.types.attrsOf reverseProxyRoutes.routeType; - default = { }; - description = "Reverse-proxy route declarations keyed by stable route name."; - }; + enable = lib.mkEnableOption "the public edge reverse proxy"; }; config = lib.mkIf cfg.enable { - assertions = - lib.mapAttrsToList (host: rootRoute: { - assertion = rootRoute != null; - message = "modules.reverse-proxy.routes for ${host} must include a primary / route."; - }) rootRoutesByHost - ++ lib.mapAttrsToList (host: routes: { - assertion = !hasDuplicatePaths routes; - message = "modules.reverse-proxy.routes for ${host} must not declare the same path more than once."; - }) routesByHost - ++ map (route: { - assertion = !hasUnresolvedBackend route; - message = "modules.reverse-proxy.routes for ${route.host}${route.path} must resolve to a backend URL before actualization."; - }) (builtins.filter hasUnresolvedBackend (lib.attrValues enabledRoutes)); - networking.firewall.allowedTCPPorts = [ 80 443 @@ -91,7 +89,7 @@ in recommendedOptimisation = true; recommendedProxySettings = true; recommendedTlsSettings = true; - virtualHosts = lib.mapAttrs mkVirtualHost routesByHost; + virtualHosts = generatedVirtualHosts; }; }; } diff --git a/registry/route.nix b/registry/route.nix new file mode 100644 index 0000000..63c7df4 --- /dev/null +++ b/registry/route.nix @@ -0,0 +1,18 @@ +{ lib }: +{ + type = lib.types.submodule { + options = { + domain = lib.mkOption { + type = lib.types.str; + example = "files.alexion.dev"; + description = "Public domain served by this reverse-proxy route entry."; + }; + + port = lib.mkOption { + type = lib.types.ints.between 1 65535; + example = 3923; + description = "Backend TCP port served by the matching address registry entry."; + }; + }; + }; +}