From 88384720010caceb446e4b6d70b63c67a548c403 Mon Sep 17 00:00:00 2001 From: alexion Date: Thu, 20 Aug 2026 20:04:34 -0400 Subject: [PATCH] feat(reverse-proxy): derive backends from endpoints --- flake.nix | 95 +++++++++++++++++++++++++++++++++++- lib/default.nix | 15 +++--- lib/guests.nix | 7 ++- lib/reverse-proxy-routes.nix | 51 +++++++++++++++++-- modules/reverse-proxy.nix | 12 ++++- 5 files changed, 166 insertions(+), 14 deletions(-) diff --git a/flake.nix b/flake.nix index 4966a82..fc01b8d 100644 --- a/flake.nix +++ b/flake.nix @@ -240,10 +240,13 @@ } ]; }; + endpoints = self.lib.networkEndpoints.collectFleetEndpoints { + pikachu = self.nixosConfigurations.pikachu; + }; fleetRoutes = self.lib.reverseProxyRoutes.collectFleetRoutes { inherit routeHost; pikachu = self.nixosConfigurations.pikachu; - }; + } endpoints; testConfig = (self.nixosConfigurations.pikachu.extendModules { specialArgs.reverseProxyFleetRoutes = fleetRoutes; @@ -264,6 +267,96 @@ touch $out ''; + reverse-proxy-endpoint-backends = + let + endpointHost = + lib.nixosSystem { + system = "x86_64-linux"; + specialArgs.my = self.lib; + modules = [ + ./modules/network.nix + { + modules.network.endpoint.address = "10.23.50.146"; + } + ]; + }; + 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"; + port = 8080; + }; + } + ]; + }; + guestRouteHost = + lib.nixosSystem { + system = "x86_64-linux"; + specialArgs.my = self.lib; + modules = [ + inputs.sops-nix.nixosModules.sops + ./modules/network.nix + ./modules/reverse-proxy.nix + (self.lib.guest { name = "files"; }) + { + networking.hostName = "pikachu"; + modules.network.vlans = [ 20 ]; + guests.files = { + enable = true; + vlan = 20; + endpoint.address = "10.23.20.126"; + reverseProxy = { + enable = true; + host = "files.alexion.dev"; + port = 3923; + }; + }; + } + ]; + }; + endpoints = self.lib.networkEndpoints.collectFleetEndpoints { + worker = endpointHost; + pikachu = guestRouteHost; + }; + fleetRoutes = self.lib.reverseProxyRoutes.collectFleetRoutes { + inherit routeHost; + pikachu = guestRouteHost; + } endpoints; + testConfig = + (lib.nixosSystem { + system = "x86_64-linux"; + specialArgs.my = self.lib; + modules = [ + ./modules/reverse-proxy.nix + { + modules.reverse-proxy = { + enable = true; + routes = fleetRoutes; + }; + } + ]; + }).config; + vhosts = testConfig.services.nginx.virtualHosts; + in + pkgs.runCommand "reverse-proxy-endpoint-backends-check" { } '' + ${ + lib.optionalString ( + !( + vhosts."worker.alexion.dev".locations."/".proxyPass == "http://10.23.50.146:8080" + && fleetRoutes."pikachu-files".endpoint == "pikachu-files" + && vhosts."files.alexion.dev".locations."/".proxyPass == "http://10.23.20.126:3923" + ) + ) "exit 1" + } + touch $out + ''; + reverse-proxy-secondary-path-requires-root = let testConfig = diff --git a/lib/default.nix b/lib/default.nix index 210a84e..b577a64 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -34,12 +34,15 @@ let ) (builtins.readDir dir) ); - specialArgs = { - inherit inputs self; - my = self.lib; - networkFleetEndpoints = self.lib.networkEndpoints.collectFleetEndpoints self.nixosConfigurations; - reverseProxyFleetRoutes = self.lib.reverseProxyRoutes.collectFleetRoutes self.nixosConfigurations; - }; + specialArgs = + let + networkFleetEndpoints = self.lib.networkEndpoints.collectFleetEndpoints self.nixosConfigurations; + in + { + inherit inputs networkFleetEndpoints self; + my = self.lib; + reverseProxyFleetRoutes = self.lib.reverseProxyRoutes.collectFleetRoutes self.nixosConfigurations networkFleetEndpoints; + }; bridgeName = id: "br-vlan${toString id}"; diff --git a/lib/guests.nix b/lib/guests.nix index f2ed181..123ba8e 100644 --- a/lib/guests.nix +++ b/lib/guests.nix @@ -295,7 +295,12 @@ let }; modules.reverse-proxy.routes.${machineName} = lib.mkIf cfg.reverseProxy.enable ( - reverseProxyRoutes.routeFields cfg.reverseProxy + reverseProxyRoutes.routeFields ( + cfg.reverseProxy + // lib.optionalAttrs (cfg.reverseProxy.backend == null && cfg.reverseProxy.endpoint == null) { + endpoint = "${config.networking.hostName}-${machineName}"; + } + ) ); assertions = [ diff --git a/lib/reverse-proxy-routes.nix b/lib/reverse-proxy-routes.nix index d6de9a1..4a40a7d 100644 --- a/lib/reverse-proxy-routes.nix +++ b/lib/reverse-proxy-routes.nix @@ -37,10 +37,30 @@ let }; backend = lib.mkOption { - type = lib.types.str; + 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; @@ -60,22 +80,45 @@ let 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: + hosts: endpoints: lib.concatMapAttrs ( hostName: host: lib.mapAttrs' ( - routeName: route: lib.nameValuePair "${hostName}-${routeName}" route + routeName: route: lib.nameValuePair "${hostName}-${routeName}" (resolvedRouteFields endpoints route) ) host.config.modules.reverse-proxy.routes ) hosts; in { - inherit collectFleetRoutes mkRouteOptions routeFields; + inherit collectFleetRoutes mkRouteOptions resolvedRouteFields routeFields; routeType = lib.types.submodule ( { name, ... }: diff --git a/modules/reverse-proxy.nix b/modules/reverse-proxy.nix index 2b36fc5..aaae1d9 100644 --- a/modules/reverse-proxy.nix +++ b/modules/reverse-proxy.nix @@ -20,6 +20,8 @@ let in lib.length paths != lib.length (lib.unique paths); + hasUnresolvedBackend = route: route.backend == null; + mkLocation = route: { proxyPass = route.backend; proxyWebsockets = route.websockets; @@ -34,7 +36,9 @@ let enableACME = rootRoute.tls; serverAliases = rootRoute.aliases; locations = lib.listToAttrs ( - map (route: lib.nameValuePair route.path (mkLocation route)) routes + map (route: lib.nameValuePair route.path (mkLocation route)) ( + builtins.filter (route: route.backend != null) routes + ) ); }; in @@ -58,7 +62,11 @@ in ++ lib.mapAttrsToList (host: routes: { assertion = !hasDuplicatePaths routes; message = "modules.reverse-proxy.routes for ${host} must not declare the same path more than once."; - }) routesByHost; + }) 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