Watch
0
0
Fork
You've already forked hyperhive
0

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.<service>.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
This commit is contained in:
atlas 2026-10-02 17:53:07 +02:00
commit 8628e0ecdd
7 changed files with 108 additions and 57 deletions

View file

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

View file

@ -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.<service>.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";

View file

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

View file

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

View file

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

View file

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