nix: drop the central-toggle conjunct from four compound gates

`services.hyperhive.enable` is going away (Refs #4500). These four blocks
ANDed it with a second condition that was already the load-bearing one:
the bridge-firewall hole asks whether an operator named any
`network.exposeHostPorts`, and the three swarm-level services ask their
own `deploy.*.enable`, each of which defaults false (swarm-ui derives
from the controller's toggle, a sibling deployment decision) and none of
which is derived from the hive toggle. So dropping the conjunct turns
nothing on by itself — the remaining condition still decides.

The way that claim fails is by something becoming unconditional, so
module-eval gets absence arms for all four, each asserted on a host with
the hive ON and on one with it OFF, plus the two controls that make the
absences mean something.

The swarm-controller arm is probed by the credential oneshot and by the
daemon's `ExecStart` rather than by the unit name: hive-tls defines an
environment key on that name, which leaves an inert fragment behind on
any hive with a CA whether or not the controller runs there.
This commit is contained in:
atlas 2026-09-19 03:30:34 +02:00 committed by mara
commit ce3b3d9467
5 changed files with 105 additions and 4 deletions

View file

@ -276,7 +276,7 @@ in
# is firewall-only by design: a host service that binds `0.0.0.0` already # is firewall-only by design: a host service that binds `0.0.0.0` already
# serves the bridge IP, so an extra bridge-IP proxy would only collide # serves the bridge IP, so an extra bridge-IP proxy would only collide
# (EADDRINUSE) with it. Merges with the [ 80 443 ] gateway ports above. # (EADDRINUSE) with it. Merges with the [ 80 443 ] gateway ports above.
(lib.mkIf (config.services.hyperhive.enable && cfg.exposeHostPorts != [ ]) { (lib.mkIf (cfg.exposeHostPorts != [ ]) {
networking.firewall.interfaces.${cfg.bridgeName}.allowedTCPPorts = cfg.exposeHostPorts; networking.firewall.interfaces.${cfg.bridgeName}.allowedTCPPorts = cfg.exposeHostPorts;
}) })
]; ];

View file

@ -698,7 +698,7 @@ in
}; };
}; };
config = lib.mkIf (config.services.hyperhive.enable && deployCfg.swarm-controller.enable) { config = lib.mkIf deployCfg.swarm-controller.enable {
# The daemon and the oneshot that mints its credential — the second one # The daemon and the oneshot that mints its credential — the second one
# failing leaves the first running and unable to authenticate anywhere. # failing leaves the first running and unable to authenticate anywhere.
services.hyperhive.swarm.otel.journaldUnits = [ services.hyperhive.swarm.otel.journaldUnits = [

View file

@ -563,7 +563,7 @@ in
}; };
}; };
config = lib.mkIf (config.services.hyperhive.enable && deployCfg.swarm-otel.enable) { config = lib.mkIf deployCfg.swarm-otel.enable {
# The gateway name, inside `deployCfg.swarm-otel.enable` — that guard is the load-bearing # The gateway name, inside `deployCfg.swarm-otel.enable` — that guard is the load-bearing
# part. Every hive in a swarm may know this collector exists, but only # part. Every hive in a swarm may know this collector exists, but only
# the host that RUNS it may claim the name; a client hive declaring the # the host that RUNS it may claim the name; a client hive declaring the

View file

@ -97,7 +97,7 @@ in
''; '';
}; };
config = lib.mkIf (config.services.hyperhive.enable && deployCfg.swarm-ui.enable) { config = lib.mkIf deployCfg.swarm-ui.enable {
assertions = [ assertions = [
{ {
# The `_` default server already answers for the hive domain # The `_` default server already answers for the hive domain

View file

@ -491,6 +491,43 @@ let
machine: machine:
machine.networking.firewall.interfaces.${machine.services.hyperhive.network.bridgeName}.allowedTCPPorts; machine.networking.firewall.interfaces.${machine.services.hyperhive.network.bridgeName}.allowedTCPPorts;
# Same list, on a host that may not declare the interface at all: the
# `[ 80 443 ]` block is what creates the attr, and it is off where the hive
# is. Reading it through `bridgePorts` would throw rather than report an
# empty exposure, which is precisely the state the cases below assert.
bridgePortsOrNone =
machine:
(machine.networking.firewall.interfaces.${machine.services.hyperhive.network.bridgeName} or {
allowedTCPPorts = [ ];
}
).allowedTCPPorts;
# A host that installs the modules and turns nothing on. The per-service
# deployment toggles are what decide whether the swarm-level services render
# here, and none of them is derived from the hive being on, so this renders
# the same absences `bare` does — the arms below assert both, because a
# service that had quietly become unconditional would still look right on
# only one of them.
centralToggleOff = hive { enable = false; };
# Ports asked for on such a host. The request is the whole condition: the
# firewall hole exists because an operator named a port, not because the
# hive is running, and an agent reaching a host service is a claim about
# the host's own listeners either way.
exposedPortsNoHive = hive {
enable = false;
network.exposeHostPorts = [ 5432 ];
};
# The swarm UI where its own toggle is on — 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.
swarmUiHere = hive {
deploy.swarm-ui.enable = true;
deploy.swarm-ui.package = pkgs.emptyDirectory;
};
baoTwoAddresses = hive { baoTwoAddresses = hive {
deploy.bao.enable = true; deploy.bao.enable = true;
deploy.bao.extraListenAddresses = [ "10.0.0.1" ]; deploy.bao.extraListenAddresses = [ "10.0.0.1" ];
@ -3032,6 +3069,70 @@ let
in in
!(s.listener ? metrics) && !(s ? telemetry); !(s.listener ? metrics) && !(s ? telemetry);
} }
{
# The four blocks below used to be gated on the hive being enabled AND
# their own condition. The second half was always the load-bearing one —
# none of these conditions is derived from the hive toggle — so these
# arms pin what the conjunct was doing: nothing. Each is written against
# a host with the hive OFF as well as one with it on, because the way a
# dropped conjunct fails is by making something unconditional, and that
# shows up as a service appearing where nothing asked for it.
name = "no bridge port is opened for an exposeHostPorts nobody set";
ok =
!(builtins.elem 5432 (bridgePortsOrNone bare))
&& !(builtins.elem 5432 (bridgePortsOrNone centralToggleOff));
}
{
# Control for the arm above, and the one place this slice is not inert:
# the ports are opened because they were named, on a host that never
# turned the hive on. The bridge firewall is the host's own, so there is
# nothing here for the hive toggle to have been protecting.
name = "a named exposeHostPorts opens its bridge port on the host's own say-so";
ok = builtins.elem 5432 (bridgePortsOrNone exposedPortsNoHive);
}
{
# `deploy.swarm-controller.enable`, which defaults false and is
# deliberately not derived from the hive toggle — a swarm has one
# controller, so the host that runs it says so itself.
#
# Probed by what the daemon needs in order to run, not by
# `? swarm-controller`: ./host-modules/hive-tls.nix defines an
# environment key on that unit name, which leaves the attr
# present-but-inert (no `ExecStart`, empty `wantedBy`) on every hive
# that has a CA — see the comment there. The credential oneshot has no
# second definer, so its absence is the unambiguous half.
name = "the swarm controller does not run unless this host is told to run it";
ok =
let
inert =
machine:
!(machine.systemd.services ? swarm-controller-credential)
&& !(
(machine.systemd.services.swarm-controller or { serviceConfig = { }; }).serviceConfig ? ExecStart
);
in
inert bare && inert centralToggleOff;
}
{
# `deploy.swarm-otel.enable`, same shape: the swarm's collector is one
# host's job, and the container is the whole of what it renders.
name = "the swarm collector container is absent unless this host is told to run it";
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";
ok =
!(bare.services.nginx.virtualHosts ? "t.local")
&& !(centralToggleOff.services.nginx.virtualHosts ? "t.local");
}
{
name = "the swarm UI claims the swarm apex where this host serves it";
ok = swarmUiHere.services.nginx.virtualHosts ? "t.local";
}
]; ];
bad = builtins.filter (c: !c.ok) cases; bad = builtins.filter (c: !c.ok) cases;