diff --git a/nix/host-modules/default.nix b/nix/host-modules/default.nix index 56f21137..cbfd7028 100644 --- a/nix/host-modules/default.nix +++ b/nix/host-modules/default.nix @@ -26,6 +26,7 @@ ./swarm-ca.nix ./swarm-controller.nix ./swarm-snapshot-store.nix + ./swarm-ui.nix ./swarm-wireguard.nix ./swarm.nix ./swarm-peers-removed.nix diff --git a/nix/host-modules/swarm-ui.nix b/nix/host-modules/swarm-ui.nix new file mode 100644 index 00000000..b1da263d --- /dev/null +++ b/nix/host-modules/swarm-ui.nix @@ -0,0 +1,93 @@ +# The swarm-level web UI: a static bundle served by the gateway's nginx, +# behind authelia. Distinct from the per-hive dashboard (hive-c0re's, on +# the hive domain) — this one answers for the swarm apex and is the +# operator's view across hives. +# +# Options only. The vhost itself is declared in hive-gateway/vhosts.nix +# alongside forge/matrix/authelia: a service module says *how* it is +# reached, the gateway says *whether this host serves it*. +{ + lib, + config, + pkgs, + ... +}: +let + cfg = config.services.hyperhive.swarm.ui; + swarmCfg = config.services.hyperhive.swarm; + hiveDomain = config.services.hyperhive.domain; +in +{ + options.services.hyperhive.swarm.ui = { + enable = lib.mkOption { + type = lib.types.bool; + default = swarmCfg.controller.enable; + defaultText = lib.literalExpression "services.hyperhive.swarm.controller.enable"; + example = true; + description = '' + Serve the swarm UI from this host. + + Derived from `swarm.controller.enable` rather than from + `enableRequiredServices`: the UI is a view onto the controller's + state and reaches it over that daemon's unix socket, so the host + that runs the controller is the host that can serve the UI. A + hive that merely *uses* a swarm has nothing to serve here. + ''; + }; + + domain = lib.mkOption { + type = lib.types.str; + default = if swarmCfg.domain == null then "swarm.invalid" else swarmCfg.domain; + defaultText = lib.literalExpression "services.hyperhive.swarm.domain"; + example = "swarm.example.com"; + description = '' + Host name the swarm UI answers on. Defaults to the swarm apex + itself — the swarm's front page is the swarm's name. + + An option rather than a hardcoded derivation so a hive can pin a + different name, the same way `swarm.forge.domain` and + `swarm.matrix.gatewayHost` can. + + Total on a null swarm domain (`.invalid`, RFC 2606) so the + required-domain assertion is what fires rather than a coercion + error naming this option — same reasoning as + `hive-network.nix`'s. + ''; + }; + + package = lib.mkOption { + type = lib.types.package; + default = pkgs.swarm-ui; + defaultText = lib.literalExpression "pkgs.swarm-ui"; + description = '' + Static build of the swarm UI. nginx serves this store path + directly — there is no server-side component beyond the + controller's own API. + ''; + }; + }; + + config = lib.mkIf (config.services.hyperhive.enable && cfg.enable) { + assertions = [ + { + # The `_` default server already answers for the hive domain + # (dashboard, per-agent routes). A second vhost claiming the same + # server_name is not an error to nginx — it picks one and logs a + # conflict — so the failure would surface as "the dashboard is + # sometimes the swarm UI", which is far harder to read than an + # eval failure naming both options. + assertion = cfg.domain != hiveDomain; + message = '' + services.hyperhive.swarm.ui.domain (${cfg.domain}) must differ + from services.hyperhive.domain (${hiveDomain}) — the hive + domain is already served by the gateway's default vhost + (dashboard + agent routes), and two vhosts claiming one + server_name silently resolve to whichever nginx picks. + + Set services.hyperhive.swarm.domain to a name distinct from + this hive's, or pin swarm.ui.domain explicitly. + ''; + } + ]; + }; +} diff --git a/nix/host-modules/swarm.nix b/nix/host-modules/swarm.nix index 4bf07940..d3502e45 100644 --- a/nix/host-modules/swarm.nix +++ b/nix/host-modules/swarm.nix @@ -39,7 +39,13 @@ let swarmCfg.forge.domain swarmCfg.matrix.gatewayHost swarmCfg.authelia.domain - ]; + ] + # The swarm UI's name is a SIBLING of the other three, not a parent of + # them — the apex is as much a name needing a certificate as + # `forge.` is, and no CA in the hierarchy issues for it + # implicitly. Left out, its vhost falls back to the hive leaf and the + # swarm's front page opens with a name mismatch. + ++ lib.optional swarmCfg.ui.enable swarmCfg.ui.domain; in { options.services.hyperhive.swarm.hives = lib.mkOption {