Watch
0
0
Fork
You've already forked hyperhive
0

nix: run swarm-ui wherever swarm-controller runs

Remove deploy.swarm-ui.enable. Every reader (the swarm-ui vhost, the
authelia operator rule for the swarm apex, hive-network's
anyServiceEnabled) now reads deploy.swarm-controller.enable, and the
controller's webhook public URL is gated on the swarm domain alone,
since the vhost it names exists wherever the daemon does.

Both spellings of the old switch (deploy.swarm-ui.enable and the
already-renamed swarm.ui.enable) become mkRemovedOptionModule entries,
so a host that still sets either fails evaluation with a message
naming the controller toggle instead of silently changing.

Ruling (mara, #3742): "swarm-ui always runs wherever controller runs".

Part of #3742
This commit is contained in:
atlas 2026-10-13 08:19:35 +02:00 • committed by mara
commit f9954b91b4
10 changed files with 83 additions and 59 deletions

View file

@ -12,7 +12,7 @@ You rarely switch it on yourself. `gateway.enable` defaults to off, and every mo
| URL | upstream | declared by, when |
| --- | --- | --- |
| `<swarm>/` | swarm-ui dist (static), behind an authelia subrequest | `swarm-ui.nix`, `deploy.swarm-ui.enable` |
| `<swarm>/` | swarm-ui dist (static), behind an authelia subrequest | `swarm-ui.nix`, `deploy.swarm-controller.enable` |
| `auth.<swarm>/` | authelia (`9091`) | `swarm-authelia.nix`, `deploy.authelia.enable` |
| `auth.<swarm>/bridge/*` | swarm-authelia-bridge (`9092`), prefix stripped; answers swarm-controller's client only | `swarm-authelia.nix`, `deploy.authelia.enable` |
| `forge.<swarm>/` | forgejo (`3000`) | `hive-forge/`, `deploy.forgejo.enable` |

View file

@ -334,10 +334,10 @@ Where each credential lives and who reads it:
## Enabling
```nix
services.hyperhive.deploy.swarm-ui.enable = true; # defaults to deploy.swarm-controller.enable
services.hyperhive.deploy.swarm-controller.enable = true; # the UI comes with it
```
Derived from the controller rather than from `allSwarmServices`:
The UI has no switch of its own, and doesn't follow `allSwarmServices`:
the UI is a view onto the controller's state and reaches it over that
daemon's 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.

View file

@ -28,9 +28,13 @@
# spread across the service modules, so all of them have a single home
# and a single file to delete when the deprecation window closes — the
# shape ./swarm-peers-removed.nix already uses.
{ lib, config, ... }:
{ lib, ... }:
let
deployCfg = config.services.hyperhive.deploy;
# Shared by the two removed spellings of the swarm UI's own switch below.
swarmUiSwitchRemoved = ''
swarm-ui now runs wherever swarm-controller runs. Remove this
definition; services.hyperhive.deploy.swarm-controller.enable decides it.
'';
# Shared by the two removed spellings of the status-publishing secret below.
statusSecretRemoved = ''
@ -94,10 +98,20 @@ in
be deleted.
''
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "ui" "enable" ]
[ "services" "hyperhive" "deploy" "swarm-ui" "enable" ]
)
(lib.mkRemovedOptionModule [
"services"
"hyperhive"
"swarm"
"ui"
"enable"
] swarmUiSwitchRemoved)
(lib.mkRemovedOptionModule [
"services"
"hyperhive"
"deploy"
"swarm-ui"
"enable"
] swarmUiSwitchRemoved)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "authelia" "enable" ]
[ "services" "hyperhive" "deploy" "authelia" "enable" ]
@ -436,9 +450,8 @@ in
[ "services" "hyperhive" "swarm" "victoriametrics" "package" ]
[ "services" "hyperhive" "deploy" "victoriametrics" "package" ]
)
# With this one `services.hyperhive.swarm.ui` holds nothing at all — its
# two options are both renamed, so the namespace exists only as this
# entry and the `enable` one above it.
# With this one `services.hyperhive.swarm.ui` holds nothing at all: its
# `enable` is removed above and its `package` is renamed here.
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "ui" "package" ]
[ "services" "hyperhive" "deploy" "swarm-ui" "package" ]
@ -688,23 +701,11 @@ in
{option}`services.hyperhive.deploy.hive-controller.enable`: a swarm
has one controller, so running it is a decision about this host
rather than about whether it runs a hive.
'';
};
swarm-ui.enable = lib.mkOption {
type = lib.types.bool;
default = deployCfg.swarm-controller.enable;
defaultText = lib.literalExpression "services.hyperhive.deploy.swarm-controller.enable";
example = true;
description = ''
Serve the swarm UI from this host.
Derived from
{option}`services.hyperhive.deploy.swarm-controller.enable`: 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.
Also serves the swarm UI from this host. 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 only host that can
serve it.
'';
};
};

View file

@ -623,7 +623,7 @@ in
# swarm's host was the missing one, and every swarm-controller
# delivery was refused for it.
#
# Not gated on `deploy.swarm-ui.enable`: in a spread deployment the
# Not gated on `deploy.swarm-controller.enable`: in a spread deployment the
# forge and the swarm host are different machines, so that
# per-host flag is false exactly where this entry is needed.
webhook.ALLOWED_HOST_LIST = lib.concatStringsSep "," (

View file

@ -40,7 +40,6 @@ let
lib.any lib.id [
d.hive-controller.enable
d.swarm-controller.enable
d.swarm-ui.enable
d.forgejo.enable
d.ci-runner.enable
d.matrix.enable

View file

@ -1107,7 +1107,7 @@ in
[ metricsRule ]
# Also guarded on the domain being set: without it a null
# apex would render a rule matching the string "null".
++ lib.optional (deployCfg.swarm-ui.enable && swarmDomain != null) {
++ lib.optional (deployCfg.swarm-controller.enable && swarmDomain != null) {
domain = swarmDomain;
subject = [ "group:${operatorGroup}" ];
policy = "one_factor";

View file

@ -204,9 +204,9 @@ let
};
# How the forge must address this controller to deliver a swarm-wide
# webhook. Gated on the swarm UI being served *here*, because that module
# is what declares the vhost and the `/webhook/forge/` location inside it:
# absent, nothing outside this host can reach the endpoint.
# webhook. ./swarm-ui.nix declares the vhost and the `/webhook/forge/`
# location inside it on every host that runs this daemon, so the only gate
# left is a swarm domain to name it by.
#
# 🔑 The gate is the point, not a detail. The daemon registers hooks only
# when this is set, and a hook whose `target_url` nothing answers is worse
@ -216,17 +216,15 @@ let
# ⚠️ Correct ONLY while the vhost and this daemon share a host, and they do
# by construction: the UI's `/api/` location proxies
# `http://unix:<socketPath>`, a path that resolves nowhere else, and this
# daemon binds no TCP address at all (see the header). `deploy.swarm-ui`
# therefore is not a guess about *some* host publishing the endpoint — it is
# the flag that declares that vhost, on the box holding the socket.
# daemon binds no TCP address at all (see the header).
#
# ⇒ The day this daemon grows a TCP listener so another host can front it,
# THIS LINE SILENTLY STOPS BEING RIGHT: the env goes unset on the controller
# host, registration quietly does not happen, and nothing errors. That is
# THIS LINE SILENTLY STOPS BEING RIGHT: hooks get registered against a vhost
# that may no longer be the one fronting the daemon, and nothing errors. That is
# the moment to add an explicit `publicUrl` option — not before, because
# until then there is exactly one derivable answer and an option would only
# be a second place to get it wrong.
webhookEnv = lib.optionalAttrs (deployCfg.swarm-ui.enable && swarmDomain != null) {
webhookEnv = lib.optionalAttrs (swarmDomain != null) {
SWARM_CONTROLLER_PUBLIC_URL = "https://${swarmDomain}";
};
@ -469,8 +467,8 @@ in
`services.hyperhive.swarm.bao.ui.domain`, and the Matrix entry
from `services.hyperhive.swarm.matrix.gatewayHost` when it is set,
so they are present whichever host runs each service.
`swarm-ui.nix` contributes its own API-docs entry wherever the
swarm UI is enabled on this host.
`swarm-ui.nix` contributes its own API-docs entry, since the
swarm UI runs wherever the controller does.
Read only on the host that runs the controller.
'';

View file

@ -66,10 +66,10 @@ let
apex = if swarmCfg.domain == null then "swarm.invalid" else swarmCfg.domain;
in
{
# `enable` is `services.hyperhive.deploy.swarm-ui.enable` — see
# ./deploy.nix, where it derives from the controller's own deploy
# toggle for the same reason. There is no `domain` option: the UI answers
# on the swarm apex, which the swarm already declares once.
# No `enable` of its own: the UI is on wherever
# `services.hyperhive.deploy.swarm-controller.enable` is, because it reaches
# that daemon over its unix socket. There is no `domain` option: the UI
# answers on the swarm apex, which the swarm already declares once.
imports = [
(lib.mkRemovedOptionModule [ "services" "hyperhive" "swarm" "ui" "domain" ] ''
The swarm UI answers on services.hyperhive.swarm.domain and nothing
@ -83,9 +83,9 @@ in
'')
];
# ⚠️ `services.hyperhive.swarm.ui` is not a namespace. Its two options,
# `enable` and `package`, live under `deploy.swarm-ui`, and the `swarm.ui`
# paths survive purely as renames in ./deploy.nix.
# ⚠️ `services.hyperhive.swarm.ui` is not a namespace. Its `package` lives
# under `deploy.swarm-ui`, and the `swarm.ui` paths survive purely as
# rename/removal entries in ./deploy.nix.
options.services.hyperhive.deploy.swarm-ui.package = lib.mkOption {
type = lib.types.package;
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-ui";
@ -102,7 +102,7 @@ in
'';
};
config = lib.mkIf deployCfg.swarm-ui.enable {
config = lib.mkIf deployCfg.swarm-controller.enable {
assertions = [
{
# The `_` default server already answers for the hive domain

View file

@ -631,11 +631,11 @@ let
ok = !(bare.containers ? swarm-otel) && !(centralToggleOff.containers ? swarm-otel);
}
{
# `deploy.swarm-ui.enable`, which defaults to the controller's toggle —
# derived from a sibling deployment decision, still not from the hive
# toggle. `t.local` is the fixtures' swarm domain, which is the apex the
# UI claims; the arm below is what proves this vhost renders at all.
name = "the swarm UI vhost is absent unless this host is told to serve it";
# The swarm UI follows `deploy.swarm-controller.enable` — a sibling
# deployment decision, not the hive toggle. `t.local` is the fixtures'
# swarm domain, which is the apex the UI claims; the arm below is what
# proves this vhost renders at all.
name = "the swarm UI vhost is absent unless this host runs the swarm controller";
ok =
!(bare.services.nginx.virtualHosts ? "t.local")
&& !(centralToggleOff.services.nginx.virtualHosts ? "t.local");

View file

@ -70,13 +70,19 @@ let
network.exposeHostPorts = [ 5432 ];
};
# The swarm UI where its own toggle is on — the control the absence arm
# The swarm UI on the controller's host — the control the absence arm
# needs, since nothing else in this suite renders this vhost and an arm
# saying "it is not there" would hold just as well if it were never there.
# Package stubbed per this file's header: the vhost roots at it.
#
# Also sets both removed UI switches to `false`: the vhost must render
# regardless, and each must fail evaluation through its assertion. Neither
# is read anywhere else, so they change nothing the other arms see.
swarmUiHere = hive {
deploy.swarm-ui.enable = true;
deploy.swarm-controller.enable = true;
deploy.swarm-ui.package = pkgs.emptyDirectory;
deploy.swarm-ui.enable = false;
swarm.ui.enable = false;
};
# The swarm's shared services hosted HERE without the all-local mode — a
@ -224,7 +230,7 @@ let
name = "hosting the swarm's shared services does not make a hive the swarm's control plane";
ok =
!swarmServicesHere.services.hyperhive.deploy.swarm-controller.enable
&& !swarmServicesHere.services.hyperhive.deploy.swarm-ui.enable;
&& !(swarmServicesHere.services.nginx.virtualHosts ? "t.local");
}
{
# Control for the arm above, and the one place this slice is not inert:
@ -235,9 +241,29 @@ let
ok = builtins.elem 5432 (bridgePortsOrNone exposedPortsNoHive);
}
{
name = "the swarm UI claims the swarm apex where this host serves it";
name = "the swarm UI claims the swarm apex wherever this host runs the swarm controller";
ok = swarmUiHere.services.nginx.virtualHosts ? "t.local";
}
{
# Control: `swarmServicesHere` sets neither removed switch.
name = "a definition of a removed swarm UI switch fails evaluation, naming the option and the controller";
ok =
let
refused =
m: path:
lib.any (
a:
!a.assertion
&& lib.hasInfix "services.hyperhive.${path}" a.message
&& lib.hasInfix "swarm-ui now runs wherever swarm-controller runs" a.message
) m.assertions;
paths = [
"deploy.swarm-ui.enable"
"swarm.ui.enable"
];
in
lib.all (refused swarmUiHere) paths && !(lib.any (refused swarmServicesHere) paths);
}
{
# The swarm-services toggle enables them explicitly, from its own
# module rather than from any of their defaults.