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

@ -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 }`, `services.hyperhive.swarm.controller.links` (a `listOf { label, icon, url }`,
same shape as the per-agent `services.hyperhive.agent.dashboardLinks`). same shape as the per-agent `services.hyperhive.agent.dashboardLinks`).
Each service's own module contributes its entry when it's enabled on the Links to swarm services come from swarm-level options, so the list is the
controller's host — `swarm-authelia.nix`, `hive-matrix.nix`, same whichever host runs each service:
`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 | link | URL built from |
a nix-only change to that service's module, or an operator adding an entry | -------- | ------------------------------------------------- |
directly. An empty list hides the button. | 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, The **Matrix** entry opens the swarm's matrix web client (fluffychat,
`services.hyperhive.deploy.matrix.gui.package`) at the homeserver's `services.hyperhive.deploy.matrix.gui.package`) at the homeserver's

View file

@ -700,18 +700,6 @@ in
authelia = "127.0.0.1:${toString cfg.metricsPort}"; 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. # `server_name = authelia.domain`, all of `/` → authelia.
# #
# ⚠️ The server name must be exactly `cfg.domain`, not a near-miss: # ⚠️ The server name must be exactly `cfg.domain`, not a near-miss:

View file

@ -18,6 +18,40 @@ let
deployCfg = config.services.hyperhive.deploy; deployCfg = config.services.hyperhive.deploy;
autheliaCfg = config.services.hyperhive.swarm.authelia; autheliaCfg = config.services.hyperhive.swarm.authelia;
natsCfg = config.services.hyperhive.swarm.nats; 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 # 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` # 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 links menu (`GET /api/links`). Same shape and same
zero-code-change-to-extend idea as `hyperhive.dashboardLinks` zero-code-change-to-extend idea as `hyperhive.dashboardLinks`
(`nix/agent-modules/dashboard-links.nix`), one level up: rather (`nix/agent-modules/dashboard-links.nix`), one level up: rather
than one central hardcoded list, each service's own module than one fixed list, definitions merge with the list-merge idiom
contributes its own entry when it is actually enabled on this `services.hyperhive.gateway.localNames` uses, so an operator can
host — `swarm-authelia.nix`, `hive-matrix.nix` and add arbitrary extra entries here directly with no swarm-controller
`hive-forge/default.nix` all do — the same list-merge idiom or swarm-ui change.
`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.
Only meaningful on the host that actually runs the controller — The Authelia, Grafana, Metrics and Logs entries come from
entries contributed on any other host are computed but never `services.hyperhive.swarm.<service>.domain`, so they are present
read. In a swarm that splits `swarm-authelia`/`hive-matrix`/ whichever host runs each service. `hive-matrix.nix`,
`hive-forge` across hosts other than the controller's, this list `hive-forge/default.nix` and `swarm-ui.nix` contribute their own
only reflects what is enabled locally; see each contributing entries, and only where they are enabled on this host.
module's own activation condition.
Read only on the host that runs the controller.
''; '';
}; };
@ -643,6 +674,8 @@ in
}; };
config = lib.mkIf deployCfg.swarm-controller.enable { config = lib.mkIf deployCfg.swarm-controller.enable {
services.hyperhive.swarm.controller.links = swarmServiceLinks;
users.users.swarm-controller = { users.users.swarm-controller = {
isSystemUser = true; isSystemUser = true;
group = "swarm-controller"; group = "swarm-controller";

View file

@ -399,14 +399,6 @@ in
services.hyperhive.gateway.enable = lib.mkDefault true; services.hyperhive.gateway.enable = lib.mkDefault true;
services.hyperhive.gateway.dns.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 # 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 # 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. # 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.enable = lib.mkDefault true;
services.hyperhive.gateway.dns.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 # Authenticated front door onto the loopback-only store — see the
# file-top comment for why this is safe to add without touching the # file-top comment for why this is safe to add without touching the
# store's own (still unauthenticated, still loopback) listener at all. # 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.enable = lib.mkDefault true;
services.hyperhive.gateway.dns.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 # 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 # listener it serves queries on, so the swarm's collector can scrape it
# with no exporter and no extra port. # with no exporter and no extra port.

View file

@ -95,6 +95,34 @@ let
deploy.forgejo.mirrors = [ aMirror ]; 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 # Every service container at once: the services-only host plus the CI
# runner, the one container on a netns of its own. # runner, the one container on a netns of its own.
serviceContainersWithCi = hive { serviceContainersWithCi = hive {
@ -194,6 +222,23 @@ let
&& !(swarmServicesOnly.systemd.sockets ? hive-priv) && !(swarmServicesOnly.systemd.sockets ? hive-priv)
&& !(s ? swarm-bao-queue-agent); && !(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 # The in-container option drives the container's firewall, and host
# `false` with container `true` would let the container's # `false` with container `true` would let the container's