From 8628e0ecdd0e05a2baf06f32d4d36cd4198fa10d Mon Sep 17 00:00:00 2001 From: atlas Date: Fri, 2 Oct 2026 17:53:07 +0200 Subject: [PATCH] swarm-ui: build authelia/grafana/metrics/logs links from swarm domains The swarm-controller module builds the Authelia, Grafana, Metrics and Logs quick links from services.hyperhive.swarm..domain, on the controller's host, instead of each service module adding its entry only on the host that runs it. A controller whose swarm runs those services on other hosts lists them in its /api/links popover. Forge, matrix and bao links are not moved yet: forge waits on #4891, matrix and bao on whether their GUI gate becomes swarm-level. Refs #4885 --- docs/swarm/ui.md | 21 +++++--- nix/host-modules/swarm-authelia.nix | 12 ----- nix/host-modules/swarm-controller.nix | 61 +++++++++++++++++----- nix/host-modules/swarm-grafana.nix | 8 --- nix/host-modules/swarm-victorialogs.nix | 8 --- nix/host-modules/swarm-victoriametrics.nix | 8 --- nix/module-eval/swarm-services-switch.nix | 45 ++++++++++++++++ 7 files changed, 107 insertions(+), 56 deletions(-) diff --git a/docs/swarm/ui.md b/docs/swarm/ui.md index a1129e00..ce7ec349 100644 --- a/docs/swarm/ui.md +++ b/docs/swarm/ui.md @@ -104,12 +104,21 @@ opening a popover of links to other swarm-wide services. Backed by `services.hyperhive.swarm.controller.links` (a `listOf { label, icon, url }`, same shape as the per-agent `services.hyperhive.agent.dashboardLinks`). -Each service's own module contributes its entry when it's enabled on the -controller's host — `swarm-authelia.nix`, `hive-matrix.nix`, -`hive-forge/default.nix`, `swarm-grafana.nix`, `swarm-victorialogs.nix`, and -`swarm-ui.nix` for this UI's own API docs. Adding a link for a new service is -a nix-only change to that service's module, or an operator adding an entry -directly. An empty list hides the button. +Links to swarm services come from swarm-level options, so the list is the +same whichever host runs each service: + +| link | URL built from | +| -------- | ------------------------------------------------- | +| Authelia | `services.hyperhive.swarm.authelia.domain` | +| Grafana | `services.hyperhive.swarm.grafana.domain` | +| Metrics | `services.hyperhive.swarm.victoriametrics.domain` | +| Logs | `services.hyperhive.swarm.victorialogs.domain` | + +`nix/host-modules/swarm-controller.nix` builds these entries. The +Matrix and Forge entries, and `swarm-ui.nix`'s entry for this UI's own API +docs, come from those services' own modules, and only when the controller's +host runs that service. An operator can add entries directly. An +empty list hides the button. The **Matrix** entry opens the swarm's matrix web client (fluffychat, `services.hyperhive.deploy.matrix.gui.package`) at the homeserver's diff --git a/nix/host-modules/swarm-authelia.nix b/nix/host-modules/swarm-authelia.nix index f42a971f..c5b2ee5c 100644 --- a/nix/host-modules/swarm-authelia.nix +++ b/nix/host-modules/swarm-authelia.nix @@ -700,18 +700,6 @@ in authelia = "127.0.0.1:${toString cfg.metricsPort}"; }; - # This swarm-ui quick-links entry, same guard as the vhost/DNS name - # above (only the host actually running the container claims it — - # see `services.hyperhive.swarm.controller.links`'s description for - # the contribute-your-own-entry idiom). - services.hyperhive.swarm.controller.links = [ - { - label = "Authelia"; - icon = "🔑"; - url = "https://${cfg.domain}/"; - } - ]; - # `server_name = authelia.domain`, all of `/` → authelia. # # ⚠️ The server name must be exactly `cfg.domain`, not a near-miss: diff --git a/nix/host-modules/swarm-controller.nix b/nix/host-modules/swarm-controller.nix index dda27489..c97a81e6 100644 --- a/nix/host-modules/swarm-controller.nix +++ b/nix/host-modules/swarm-controller.nix @@ -18,6 +18,40 @@ let deployCfg = config.services.hyperhive.deploy; autheliaCfg = config.services.hyperhive.swarm.authelia; natsCfg = config.services.hyperhive.swarm.nats; + swarmCfg = config.services.hyperhive.swarm; + + # Quick links to swarm services, built from `swarm.*` options alone: every + # host evaluates the same entries, whichever host runs each service. One + # entry per service; reading a `deploy.*` option here would tie a link to + # where its service runs. + swarmServiceLinks = + map + (s: { + inherit (s) label icon; + url = "https://${s.domain}/"; + }) + [ + { + label = "Authelia"; + icon = "🔑"; + inherit (swarmCfg.authelia) domain; + } + { + label = "Grafana"; + icon = "📊"; + inherit (swarmCfg.grafana) domain; + } + { + label = "Metrics"; + icon = "📈"; + inherit (swarmCfg.victoriametrics) domain; + } + { + label = "Logs"; + icon = "📜"; + inherit (swarmCfg.victorialogs) domain; + } + ]; # Where the secret store is, and whether this host holds the controller's # own leaf for it. ⚠️ The controller's pair, NOT `deploy.bao.clientCertFile` @@ -411,21 +445,18 @@ in links menu (`GET /api/links`). Same shape and same zero-code-change-to-extend idea as `hyperhive.dashboardLinks` (`nix/agent-modules/dashboard-links.nix`), one level up: rather - than one central hardcoded list, each service's own module - contributes its own entry when it is actually enabled on this - host — `swarm-authelia.nix`, `hive-matrix.nix` and - `hive-forge/default.nix` all do — the same list-merge idiom - `services.hyperhive.gateway.localNames` already uses. A future - service module can push its own entry the same way, and an - operator can add arbitrary extra entries here directly; neither - needs a swarm-controller or swarm-ui change. + than one fixed list, definitions merge with the list-merge idiom + `services.hyperhive.gateway.localNames` uses, so an operator can + add arbitrary extra entries here directly with no swarm-controller + or swarm-ui change. - Only meaningful on the host that actually runs the controller — - entries contributed on any other host are computed but never - read. In a swarm that splits `swarm-authelia`/`hive-matrix`/ - `hive-forge` across hosts other than the controller's, this list - only reflects what is enabled locally; see each contributing - module's own activation condition. + The Authelia, Grafana, Metrics and Logs entries come from + `services.hyperhive.swarm..domain`, so they are present + whichever host runs each service. `hive-matrix.nix`, + `hive-forge/default.nix` and `swarm-ui.nix` contribute their own + entries, and only where they are enabled on this host. + + Read only on the host that runs the controller. ''; }; @@ -643,6 +674,8 @@ in }; config = lib.mkIf deployCfg.swarm-controller.enable { + services.hyperhive.swarm.controller.links = swarmServiceLinks; + users.users.swarm-controller = { isSystemUser = true; group = "swarm-controller"; diff --git a/nix/host-modules/swarm-grafana.nix b/nix/host-modules/swarm-grafana.nix index 547ed043..2de1b2b3 100644 --- a/nix/host-modules/swarm-grafana.nix +++ b/nix/host-modules/swarm-grafana.nix @@ -399,14 +399,6 @@ in services.hyperhive.gateway.enable = lib.mkDefault true; services.hyperhive.gateway.dns.enable = lib.mkDefault true; - services.hyperhive.swarm.controller.links = [ - { - label = "Grafana"; - icon = "📊"; - url = "https://${cfg.domain}/"; - } - ]; - # Registering the client is NOT here any more: it has to happen on the # host that runs authelia, and this whole block is gated on the host that # runs Grafana. ./glue-grafana-oidc-client.nix is where it moved to. diff --git a/nix/host-modules/swarm-victorialogs.nix b/nix/host-modules/swarm-victorialogs.nix index a295fd21..713f2e4d 100644 --- a/nix/host-modules/swarm-victorialogs.nix +++ b/nix/host-modules/swarm-victorialogs.nix @@ -112,14 +112,6 @@ in services.hyperhive.gateway.enable = lib.mkDefault true; services.hyperhive.gateway.dns.enable = lib.mkDefault true; - services.hyperhive.swarm.controller.links = [ - { - label = "Logs"; - icon = "📜"; - url = "https://${cfg.domain}/"; - } - ]; - # Authenticated front door onto the loopback-only store — see the # file-top comment for why this is safe to add without touching the # store's own (still unauthenticated, still loopback) listener at all. diff --git a/nix/host-modules/swarm-victoriametrics.nix b/nix/host-modules/swarm-victoriametrics.nix index de8501db..7fbf9b22 100644 --- a/nix/host-modules/swarm-victoriametrics.nix +++ b/nix/host-modules/swarm-victoriametrics.nix @@ -71,14 +71,6 @@ in services.hyperhive.gateway.enable = lib.mkDefault true; services.hyperhive.gateway.dns.enable = lib.mkDefault true; - services.hyperhive.swarm.controller.links = [ - { - label = "Metrics"; - icon = "📈"; - url = "https://${cfg.domain}/"; - } - ]; - # This store publishes its own health as prometheus metrics on the same # listener it serves queries on, so the swarm's collector can scrape it # with no exporter and no extra port. diff --git a/nix/module-eval/swarm-services-switch.nix b/nix/module-eval/swarm-services-switch.nix index 3d589ac8..7d68a1c2 100644 --- a/nix/module-eval/swarm-services-switch.nix +++ b/nix/module-eval/swarm-services-switch.nix @@ -95,6 +95,34 @@ let deploy.forgejo.mirrors = [ aMirror ]; }; + # The controller with every shared service on another host, and the same + # controller hosting them all: the swarm UI's links must not tell the two + # apart. + controllerAlone = hive { deploy.swarm-controller.enable = true; }; + controllerWithServices = hive { + deploy.swarm-controller.enable = true; + deploy.allSwarmServices = true; + }; + + # Each shared service with a web UI, and the link it must have: the label + # and the swarm-level domain the URL is built from. + swarmServiceLinkDomains = + let + s = controllerAlone.services.hyperhive.swarm; + in + { + Authelia = s.authelia.domain; + Grafana = s.grafana.domain; + Metrics = s.victoriametrics.domain; + Logs = s.victorialogs.domain; + # forge: added once hive-forge/default.nix drops its per-host link + # matrix: added once its GUI gate is settled + # bao: added once its UI gate is settled + }; + swarmServiceLinksOf = + cfg: + lib.filter (l: swarmServiceLinkDomains ? ${l.label}) cfg.services.hyperhive.swarm.controller.links; + # Every service container at once: the services-only host plus the CI # runner, the one container on a netns of its own. serviceContainersWithCi = hive { @@ -194,6 +222,23 @@ let && !(swarmServicesOnly.systemd.sockets ? hive-priv) && !(s ? swarm-bao-queue-agent); } + { + # On a controller that runs none of them, so the link cannot come from + # the service's own module. + name = "the swarm UI links every shared service with a web UI at its swarm domain"; + ok = + lib.listToAttrs (map (l: lib.nameValuePair l.label l.url) (swarmServiceLinksOf controllerAlone)) + == lib.mapAttrs (_: domain: "https://${domain}/") swarmServiceLinkDomains; + } + { + # Exact list equality also catches an entry rendered twice where the + # service runs on the controller's own host. + name = "the swarm UI's service links are the same whichever host runs the services"; + ok = + lib.length (swarmServiceLinksOf controllerAlone) + == lib.length (lib.attrNames swarmServiceLinkDomains) + && swarmServiceLinksOf controllerWithServices == swarmServiceLinksOf controllerAlone; + } { # The in-container option drives the container's firewall, and host # `false` with container `true` would let the container's