feat(reverse-proxy): derive backends from endpoints #52

Merged
alexion merged 1 commits from impl/reverse-proxy-endpoint-backends into main 2026-08-22 12:44:48 -04:00
5 changed files with 166 additions and 14 deletions

View File

@@ -240,10 +240,13 @@
} }
]; ];
}; };
endpoints = self.lib.networkEndpoints.collectFleetEndpoints {
pikachu = self.nixosConfigurations.pikachu;
};
fleetRoutes = self.lib.reverseProxyRoutes.collectFleetRoutes { fleetRoutes = self.lib.reverseProxyRoutes.collectFleetRoutes {
inherit routeHost; inherit routeHost;
pikachu = self.nixosConfigurations.pikachu; pikachu = self.nixosConfigurations.pikachu;
}; } endpoints;
testConfig = testConfig =
(self.nixosConfigurations.pikachu.extendModules { (self.nixosConfigurations.pikachu.extendModules {
specialArgs.reverseProxyFleetRoutes = fleetRoutes; specialArgs.reverseProxyFleetRoutes = fleetRoutes;
@@ -264,6 +267,96 @@
touch $out 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 = reverse-proxy-secondary-path-requires-root =
let let
testConfig = testConfig =

View File

@@ -34,12 +34,15 @@ let
) (builtins.readDir dir) ) (builtins.readDir dir)
); );
specialArgs = { specialArgs =
inherit inputs self; let
my = self.lib; networkFleetEndpoints = self.lib.networkEndpoints.collectFleetEndpoints self.nixosConfigurations;
networkFleetEndpoints = self.lib.networkEndpoints.collectFleetEndpoints self.nixosConfigurations; in
reverseProxyFleetRoutes = self.lib.reverseProxyRoutes.collectFleetRoutes self.nixosConfigurations; {
}; inherit inputs networkFleetEndpoints self;
my = self.lib;
reverseProxyFleetRoutes = self.lib.reverseProxyRoutes.collectFleetRoutes self.nixosConfigurations networkFleetEndpoints;
};
bridgeName = id: "br-vlan${toString id}"; bridgeName = id: "br-vlan${toString id}";

View File

@@ -295,7 +295,12 @@ let
}; };
modules.reverse-proxy.routes.${machineName} = lib.mkIf cfg.reverseProxy.enable ( 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 = [ assertions = [

View File

@@ -37,10 +37,30 @@ let
}; };
backend = lib.mkOption { 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."; 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 { tls = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = true; default = true;
@@ -60,22 +80,45 @@ let
aliases aliases
path path
backend backend
endpoint
port
scheme
tls tls
websockets 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 = collectFleetRoutes =
hosts: hosts: endpoints:
lib.concatMapAttrs ( lib.concatMapAttrs (
hostName: host: hostName: host:
lib.mapAttrs' ( lib.mapAttrs' (
routeName: route: lib.nameValuePair "${hostName}-${routeName}" route routeName: route: lib.nameValuePair "${hostName}-${routeName}" (resolvedRouteFields endpoints route)
) host.config.modules.reverse-proxy.routes ) host.config.modules.reverse-proxy.routes
) hosts; ) hosts;
in in
{ {
inherit collectFleetRoutes mkRouteOptions routeFields; inherit collectFleetRoutes mkRouteOptions resolvedRouteFields routeFields;
routeType = lib.types.submodule ( routeType = lib.types.submodule (
{ name, ... }: { name, ... }:

View File

@@ -20,6 +20,8 @@ let
in in
lib.length paths != lib.length (lib.unique paths); lib.length paths != lib.length (lib.unique paths);
hasUnresolvedBackend = route: route.backend == null;
mkLocation = route: { mkLocation = route: {
proxyPass = route.backend; proxyPass = route.backend;
proxyWebsockets = route.websockets; proxyWebsockets = route.websockets;
@@ -34,7 +36,9 @@ let
enableACME = rootRoute.tls; enableACME = rootRoute.tls;
serverAliases = rootRoute.aliases; serverAliases = rootRoute.aliases;
locations = lib.listToAttrs ( 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 in
@@ -58,7 +62,11 @@ in
++ lib.mapAttrsToList (host: routes: { ++ lib.mapAttrsToList (host: routes: {
assertion = !hasDuplicatePaths routes; assertion = !hasDuplicatePaths routes;
message = "modules.reverse-proxy.routes for ${host} must not declare the same path more than once."; 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 = [ networking.firewall.allowedTCPPorts = [
80 80