swarm: remove swarm.ui.domain, serve the UI on the swarm domain
The UI and the swarm-controller it fronts are one service to a reader and to a
certificate, so a separate option only ever created two spellings of one
address — and the controller's own public URL was built from one of them while
an allow-list entry named the other.
Removed rather than aliased. A config still setting it was pinning a name the
controller never used; an alias would preserve exactly that mismatch, so
`mkRemovedOptionModule` says so and fails the build.
⚠️ The option was TOTAL and `swarm.domain` is NULLABLE. That is not incidental:
`ui.domain` fell back to `swarm.invalid` so hive-network.nix's required-domain
assertion is what an operator sees, rather than a coercion error from a
consumer. Each site keeps that property its own way — swarm-ui binds a local
total (the idiom swarm-otel.nix already uses as `domainBase`), and the three
sites already gated on `deploy.swarm-ui.enable` gain `&& domain != null`, which
is stricter than a placeholder and puts no invented name into rendered config.
One behaviour change worth naming: `swarm.nix`'s certificate SAN list used to
receive the placeholder `swarm.invalid` on a null domain and now receives
nothing. `lib.optional` would otherwise have put a literal `null` in it.
Verified with `checks.module-eval` rather than `nix-instantiate --parse`: parse
proves syntax, and every failure this change could plausibly cause — a dead
binding, a renamed reference — is an evaluation error.
This commit is contained in:
parent
3f98bfb09d
commit
5aef2d1afc
7 changed files with 43 additions and 44 deletions
|
|
@ -361,7 +361,7 @@ it is stored in the queue, not in the daemon.
|
|||
At startup the controller registers two Forgejo hooks pointing at
|
||||
itself — a `push` hook on `internal/knowledge` and a `pull_request` hook
|
||||
on the `agent-configs` org, both under
|
||||
`https://<swarm.ui.domain>/webhook/forge/`.
|
||||
`https://<swarm.domain>/webhook/forge/`.
|
||||
|
||||
The controller **interprets** a delivery and sends hives a specific
|
||||
message — *the knowledge repo changed*, *deploy agent `foo` at rev
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ 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.
|
||||
|
||||
`swarm.ui.domain` defaults to the swarm apex and can be pinned, the same
|
||||
way `swarm.forge.domain` and `swarm.matrix.gatewayHost` can.
|
||||
The UI answers on `services.hyperhive.swarm.domain` and nothing else. It
|
||||
shares that name with the swarm-controller it fronts — one service to a
|
||||
reader and to a certificate — so there is no separate option to pin.
|
||||
|
||||
> The apex must differ from `services.hyperhive.domain`. The gateway's
|
||||
> default server already answers for the hive domain, and two vhosts
|
||||
|
|
|
|||
|
|
@ -817,15 +817,13 @@ in
|
|||
# Not gated on `deploy.swarm-ui.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.
|
||||
# `swarm.ui.domain` is the option the controller builds its public
|
||||
# URL from; it is slated to collapse into `swarm.domain`.
|
||||
webhook.ALLOWED_HOST_LIST = lib.concatStringsSep "," (
|
||||
lib.unique (
|
||||
[
|
||||
"external"
|
||||
hyperhiveDomain
|
||||
]
|
||||
++ lib.optional (swarmDomain != null) config.services.hyperhive.swarm.ui.domain
|
||||
++ lib.optional (swarmDomain != null) swarmDomain
|
||||
)
|
||||
);
|
||||
log.LEVEL = "Warn";
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ let
|
|||
gatewayCfg = hyperhiveCfg.gateway;
|
||||
hyperhiveDomain = hyperhiveCfg.domain;
|
||||
swarmDomain = hyperhiveCfg.swarm.domain;
|
||||
uiCfg = hyperhiveCfg.swarm.ui;
|
||||
deployCfg = hyperhiveCfg.deploy;
|
||||
forgeCfg = hyperhiveCfg.swarm.forge;
|
||||
|
||||
|
|
@ -1399,8 +1398,10 @@ in
|
|||
# collector is registered — see `metricsRule` above,
|
||||
# which is where the reasoning for both halves lives.
|
||||
lib.optional forgeCfg.behindGateway metricsRule
|
||||
++ lib.optional deployCfg.swarm-ui.enable {
|
||||
domain = uiCfg.domain;
|
||||
# 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) {
|
||||
domain = swarmDomain;
|
||||
subject = [ "group:${operatorGroup}" ];
|
||||
policy = "one_factor";
|
||||
};
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ let
|
|||
|
||||
natsCfg = config.services.hyperhive.swarm.nats;
|
||||
forgeCfg = config.services.hyperhive.swarm.forge;
|
||||
uiCfg = config.services.hyperhive.swarm.ui;
|
||||
swarmDomain = config.services.hyperhive.swarm.domain;
|
||||
|
||||
# The controller's forge client speaks TLS to `https://${forgeCfg.domain}`,
|
||||
# which the gateway serves with a leaf signed by the hive CA — a CA
|
||||
|
|
@ -130,8 +130,8 @@ let
|
|||
# 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 {
|
||||
SWARM_CONTROLLER_PUBLIC_URL = "https://${uiCfg.domain}";
|
||||
webhookEnv = lib.optionalAttrs (deployCfg.swarm-ui.enable && swarmDomain != null) {
|
||||
SWARM_CONTROLLER_PUBLIC_URL = "https://${swarmDomain}";
|
||||
};
|
||||
|
||||
# Not a secret to deliver — `swarm-authelia-bridge`'s own bearer check
|
||||
|
|
|
|||
|
|
@ -55,33 +55,32 @@ let
|
|||
'';
|
||||
swarmCfg = config.services.hyperhive.swarm;
|
||||
hiveDomain = config.services.hyperhive.domain;
|
||||
|
||||
# The swarm apex this UI answers on. Total on a null domain (`.invalid`,
|
||||
# RFC 2606) so `hive-network.nix`'s required-domain assertion is what fires,
|
||||
# rather than a coercion error from here — same idiom as `swarm-otel.nix`'s
|
||||
# `domainBase`.
|
||||
apex = if swarmCfg.domain == null then "swarm.invalid" else swarmCfg.domain;
|
||||
in
|
||||
{
|
||||
# `enable` moved to `services.hyperhive.deploy.swarm-ui.enable` — see
|
||||
# ./deploy.nix, where it still derives from the controller's own deploy
|
||||
# toggle for the same reason. What stays here is what the UI IS: its
|
||||
# domain and wiring.
|
||||
# toggle for the same reason. `domain` is gone too: 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
|
||||
else. It shares that name with the swarm-controller it fronts — they
|
||||
are one service to a reader and to a certificate — so a second option
|
||||
only ever created two spellings of one address.
|
||||
|
||||
Removed rather than aliased: a config still setting it was pinning a
|
||||
name the controller's own public URL never used, and silently keeping
|
||||
the alias would preserve exactly that mismatch.
|
||||
'')
|
||||
];
|
||||
|
||||
options.services.hyperhive.swarm.ui = {
|
||||
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;
|
||||
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-ui";
|
||||
|
|
@ -108,16 +107,16 @@ in
|
|||
# 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;
|
||||
assertion = apex != 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.
|
||||
services.hyperhive.swarm.domain (${apex}) 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.
|
||||
this hive's.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
|
@ -132,7 +131,7 @@ in
|
|||
# child of anything the resolver already answers for, so the
|
||||
# `/<hive domain>/` rule does not cover it and this record is what
|
||||
# makes the name resolve at all.
|
||||
services.hyperhive.gateway.localNames = [ cfg.domain ];
|
||||
services.hyperhive.gateway.localNames = [ apex ];
|
||||
|
||||
# This UI's own swagger docs, always same-origin (`/api/docs/` below)
|
||||
# so — unlike authelia/matrix/forge's entries — this one needs no
|
||||
|
|
@ -172,8 +171,8 @@ in
|
|||
# than broken, so they keep `addSSL` and the asymmetry stays local
|
||||
# to the vhost whose correctness depends on the scheme.
|
||||
# `removeAttrs` because nixos asserts on a vhost declaring both.
|
||||
services.nginx.virtualHosts."${cfg.domain}" =
|
||||
(builtins.removeAttrs (gatewayCfg.lib.tlsFor cfg.domain) [ "addSSL" ])
|
||||
services.nginx.virtualHosts."${apex}" =
|
||||
(builtins.removeAttrs (gatewayCfg.lib.tlsFor apex) [ "addSSL" ])
|
||||
// {
|
||||
forceSSL = true;
|
||||
listen = gatewayCfg.lib.listen;
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ let
|
|||
# `forge.<apex>` 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 deployCfg.swarm-ui.enable swarmCfg.ui.domain
|
||||
++ lib.optional (deployCfg.swarm-ui.enable && swarmCfg.domain != null) swarmCfg.domain
|
||||
# Every swarm service that claims a gateway name belongs here, and
|
||||
# these three were missing it. Membership is what `gateway.lib.tlsFor`
|
||||
# consults to pick the services leaf over the hive one, so a name
|
||||
|
|
|
|||
Loading…
Reference in a new issue