refactor(reverse-proxy): use route registry

This commit is contained in:
2026-08-23 23:44:22 -04:00
parent c267a2ec5c
commit 7f6d3b295e
9 changed files with 186 additions and 235 deletions

View File

@@ -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.
Bind optional special arguments through `@args` and use `args.registry or config.registry` inside the module body instead.

102
flake.nix
View File

@@ -198,14 +198,13 @@
system = "x86_64-linux";
specialArgs.my = self.lib;
modules = [
self.lib.registry.module
./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;
registry.route.test."budget.alexion.dev" = {
path = "/";
backend = "http://10.23.20.42:5006";
};
}
];
@@ -224,7 +223,7 @@
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" { } ''
@@ -233,7 +232,6 @@
!(
route.host == "budget.alexion.dev"
&& route.backend == null
&& route.endpoint == "pikachu-actualbudget"
&& route.port == 5006
&& route.tls
&& route.aliases == [ ]
@@ -361,25 +359,25 @@
system = "x86_64-linux";
specialArgs.my = self.lib;
modules = [
./modules/reverse-proxy.nix
self.lib.registry.module
{
modules.reverse-proxy.routes.app = {
registry.route.routeHost.app = {
host = "worker.alexion.dev";
backend = "http://10.23.20.126: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;
specialArgs.registry = registry;
}).config;
vhosts = testConfig.containers.reverse-proxy.config.services.nginx.virtualHosts;
in
@@ -387,8 +385,9 @@
${
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 +397,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 +407,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 = {
registry.route.worker.self = {
host = "worker.alexion.dev";
endpoint = "worker";
port = 8080;
};
system.stateVersion = "26.11";
}
];
};
@@ -454,36 +442,37 @@
}
];
};
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;
workerFacts = self.lib.registry.lookup registry "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"
workerFacts.address == "10.23.50.146"
&& workerFacts.route.host == "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"
@@ -498,15 +487,14 @@
system = "x86_64-linux";
specialArgs.my = self.lib;
modules = [
self.lib.registry.module
./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";
};
modules.reverse-proxy.enable = true;
registry.route.raichu.files = {
host = "files.alexion.dev";
path = "/raichu";
backend = "http://10.23.20.10:3923";
};
}
];
@@ -514,7 +502,7 @@
hasFailedRootAssertion = lib.any (
assertion:
assertion.assertion == false
&& assertion.message == "modules.reverse-proxy.routes for files.alexion.dev must include a primary / route."
&& assertion.message == "registry.route entries for files.alexion.dev must include a primary / route."
) testConfig.assertions;
in
pkgs.runCommand "reverse-proxy-secondary-path-requires-root-check" { } ''

View File

@@ -2,7 +2,6 @@
config,
lib,
my,
reverseProxyFleetRoutes ? config.modules.reverse-proxy.routes,
...
}:
let
@@ -18,12 +17,13 @@ let
"todo.alexion.dev"
"wings.alexion.dev"
];
legacyRoutes = lib.listToAttrs (
legacyVirtualHosts = lib.listToAttrs (
map (host: {
name = "legacy-${lib.replaceStrings [ "." ] [ "-" ] host}";
name = host;
value = {
inherit host;
backend = legacyBackend;
forceSSL = true;
enableACME = true;
locations."/".proxyPass = legacyBackend;
};
}) legacyHosts
);
@@ -33,10 +33,9 @@ in
(my.guest {
name = "reverse-proxy";
interior = {
modules.reverse-proxy = {
enable = true;
routes = reverseProxyFleetRoutes // legacyRoutes;
};
modules.reverse-proxy.enable = true;
services.nginx.virtualHosts = legacyVirtualHosts;
networking.firewall.allowedTCPPorts = [ 2022 ];

View File

@@ -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;

View File

@@ -3,7 +3,6 @@
self,
specialArgs,
bridgeName,
reverseProxyRoutes,
}:
let
inherit (lib) escapeShellArg mapAttrsToList;
@@ -21,7 +20,7 @@ let
name,
interior ? { },
}:
{ config, lib, ... }:
{ config, lib, ... }@args:
let
optionPath = [ "guests" ] ++ lib.splitString "." name;
cfg = lib.getAttrFromPath optionPath config;
@@ -130,9 +129,19 @@ 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";
host = lib.mkOption {
type = lib.types.str;
description = "Primary hostname served by this guest's reverse-proxy route.";
};
port = lib.mkOption {
type = lib.types.ints.between 1 65535;
example = 3923;
description = "Backend TCP port served by this guest.";
};
};
mounts = lib.mkOption {
type = lib.types.attrsOf (
@@ -279,14 +288,15 @@ 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 {
inherit (cfg.reverseProxy) host port;
aliases = [ ];
path = "/";
backend = null;
scheme = "http";
tls = true;
websockets = false;
};
assertions = [
{
@@ -346,7 +356,7 @@ let
bindMounts = userMounts // secretMounts;
inherit specialArgs;
specialArgs = specialArgs // lib.optionalAttrs (args ? registry) { inherit (args) registry; };
config = {
imports = [

View File

@@ -32,6 +32,11 @@ in
{
inherit definitions;
lookup = registry: hostName: entryName:
filterAttrs (_name: value: value != null) (
mapAttrs (_name: hostEntriesByName: (hostEntriesByName.${hostName} or { }).${entryName} or null) registry
);
module = {
options.registry = mapAttrs (_name: registry: registryOption registry) definitions;
};

View File

@@ -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; };
}
);
}

View File

@@ -1,10 +1,33 @@
{ config, lib, my, ... }:
{
config,
lib,
my,
...
}@args:
let
cfg = config.modules.reverse-proxy;
inherit (my) reverseProxyRoutes;
registry = args.registry or config.registry;
enabledRoutes = lib.filterAttrs (_: route: route.enable) cfg.routes;
routesByHost = lib.groupBy (route: route.host) (lib.attrValues enabledRoutes);
routeWithBackend = hostName: routeName: route:
let
facts = my.registry.lookup registry hostName routeName;
in
if route.backend != null then
route
else if facts ? address && route.port != null then
route // { backend = "${route.scheme}://${facts.address}:${toString route.port}"; }
else
route;
allRoutes = lib.concatLists (
lib.mapAttrsToList (
hostName: routes:
lib.mapAttrsToList (routeName: route: routeWithBackend hostName routeName route) routes
) registry.route
);
enabledRoutes = builtins.filter (route: route.enable) allRoutes;
routesByHost = lib.groupBy (route: route.host) enabledRoutes;
rootRoutesByHost = lib.mapAttrs (
host: routes:
@@ -46,27 +69,22 @@ 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.";
};
};
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.";
message = "registry.route entries 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.";
message = "registry.route entries 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));
message = "registry.route entry for ${route.host}${route.path} must resolve to a backend URL before actualization.";
}) (builtins.filter hasUnresolvedBackend enabledRoutes);
networking.firewall.allowedTCPPorts = [
80

65
registry/route.nix Normal file
View File

@@ -0,0 +1,65 @@
{ lib }:
{
type = lib.types.submodule (
{ name, ... }:
{
options = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether this reverse-proxy route entry is active.";
};
host = lib.mkOption {
type = lib.types.str;
default = name;
defaultText = lib.literalMD "the route attribute name";
description = "Primary hostname served by this reverse-proxy route entry.";
};
aliases = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Additional hostnames served by this reverse-proxy route entry.";
};
path = lib.mkOption {
type = lib.types.str;
default = "/";
description = "URL path prefix proxied by this reverse-proxy route entry.";
};
backend = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Full HTTP backend URL that receives proxied traffic.";
};
port = lib.mkOption {
type = lib.types.nullOr (lib.types.ints.between 1 65535);
default = null;
example = 3923;
description = "Backend TCP port used with the matching address registry entry.";
};
scheme = lib.mkOption {
type = lib.types.str;
default = "http";
description = "Backend URL scheme used with the matching address registry entry.";
};
tls = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to serve this reverse-proxy route entry with ACME-backed TLS.";
};
websockets = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to enable websocket proxy headers for this reverse-proxy route entry.";
};
};
}
);
}