Watch
0
0
Fork
You've already forked hyperhive
0

fix(nix): require swarm domain only when a hyperhive service is enabled

The swarm.domain assertion in hive-network.nix fired on every host that
imported the module, so a host that enables nothing failed eval. It now
fires only when one of the hyperhive service switches is on (every
deploy.*.enable that runs something, gateway, gateway.dns, network,
otel, snapshotStore). The requirement itself is unchanged: any host that
runs a hyperhive service still needs swarm.domain.

The core-toggle module-eval suite gains a case: a missing swarm.domain is
refused on a hive and on a swarm-service-only host, and a host enabling
nothing passes every assertion.

Closes #4887
This commit is contained in:
atlas 2026-10-02 13:09:45 +02:00
commit fb2fff0668
4 changed files with 71 additions and 13 deletions

View file

@ -310,8 +310,9 @@ services.hyperhive = {
<!-- vale write-good.Passive = NO --> <!-- vale write-good.Passive = NO -->
Swarm options are identical on every host in the swarm, so `swarm.domain` is Swarm options are identical on every host in the swarm, so `swarm.domain` is
**required** on every host; `hiveName` is required on a host that runs a **required** on every host that runs any hyperhive service; a host that
hive, the secret store or the homeserver. Eval fails with a hint naming imports the module and enables nothing evaluates without it. `hiveName` is
required on a host that runs a hive, the secret store or the homeserver. Eval fails with a hint naming
each. Neither defaults, because a guessed value here is a wrong hostname each. Neither defaults, because a guessed value here is a wrong hostname
that evaluates cleanly and deploys. that evaluates cleanly and deploys.

View file

@ -27,6 +27,38 @@ let
hostCount = pow2 (32 - cfg.bridgePrefixLength); hostCount = pow2 (32 - cfg.bridgePrefixLength);
# Mask off host bits to get the network base address. # Mask off host bits to get the network base address.
networkBase = builtins.bitAnd (ipToInt cfg.bridgeIp) (4294967295 - hostCount + 1); networkBase = builtins.bitAnd (ipToInt cfg.bridgeIp) (4294967295 - hostCount + 1);
# Every switch that runs a hyperhive service on this host. Left out:
# `github.enable` (on by default, runs nothing by itself), the gateway's
# and matrix's sub-features, and `allSwarmServices`/`singleHostSwarm`,
# which only take effect through the switches listed here.
anyServiceEnabled =
let
h = config.services.hyperhive;
d = h.deploy;
in
lib.any lib.id [
d.hive-controller.enable
d.swarm-controller.enable
d.swarm-ui.enable
d.forgejo.enable
d.forgejo.ci.enable
d.matrix.enable
d.authelia.enable
d.bao.enable
d.nats.enable
d.swarm-otel.enable
d.grafana.enable
d.victoriametrics.enable
d.victorialogs.enable
d.swarm-secret-publisher.enable
d.wireguard.enable
h.gateway.enable
h.gateway.dns.enable
h.network.enable
h.otel.enable
h.snapshotStore.enable
];
in in
{ {
# Hive-internal network — the host-side bridge every container in a # Hive-internal network — the host-side bridge every container in a
@ -187,25 +219,26 @@ in
]; ];
}) })
# Un-gated: the swarm's options are identical on every host, so a host # Gated on any hyperhive service, not only the hive: the swarm's options
# running no hive and no swarm service needs the value too. The # are identical on every host, so a host running only a swarm service
# `.invalid` fallbacks described above keep this message reachable. # needs the value too. The `.invalid` fallbacks described above keep
{ # this message reachable.
(lib.mkIf anyServiceEnabled {
assertions = [ assertions = [
{ {
assertion = config.services.hyperhive.swarm.domain != null; assertion = config.services.hyperhive.swarm.domain != null;
message = '' message = ''
hyperhive requires services.hyperhive.swarm.domain to be set hyperhive requires services.hyperhive.swarm.domain to be set
on every host, to the same value on every host of the swarm — on every host that runs a hyperhive service, to the same value
the DNS domain of the swarm, of which each hive occupies one on every host of the swarm — the DNS domain of the swarm, of
sub-domain. A guessed value would be a wrong hostname that which each hive occupies one sub-domain. A guessed value would
evaluates cleanly and deploys. Set it be a wrong hostname that evaluates cleanly and deploys. Set it
(`services.hyperhive.swarm.domain = "example.com";`) — with (`services.hyperhive.swarm.domain = "example.com";`) — with
`hiveName` it also derives `services.hyperhive.domain` for you. `hiveName` it also derives `services.hyperhive.domain` for you.
''; '';
} }
]; ];
} })
# The bridge itself, up only where something hangs off it. Private # The bridge itself, up only where something hangs off it. Private
# netns is still the only container mode. # netns is still the only container mode.

View file

@ -117,8 +117,10 @@ in
`services.hyperhive.hiveName`, list the hives by name, and no `services.hyperhive.hiveName`, list the hives by name, and no
hive in the swarm states an address at all. hive in the swarm states an address at all.
**Required** on every host that imports this module, with the **Required** on every host that runs a hyperhive service (a
same value on every host of the swarm, and deliberately not `services.hyperhive.deploy.*` service, the gateway, the bridge,
`otel` or the snapshot store), with the same value on every host
of the swarm, and deliberately not
defaulted. A guessed swarm domain is a wrong hostname that defaulted. A guessed swarm domain is a wrong hostname that
evaluates cleanly and deploys, which is worse than an eval evaluates cleanly and deploys, which is worse than an eval
failure telling an operator to write down the one address their failure telling an operator to write down the one address their

View file

@ -503,6 +503,28 @@ let
} }
&& !(refusedHiveName { deploy.hive-controller.enable = false; }); && !(refusedHiveName { deploy.hive-controller.enable = false; });
} }
{
# Refused wherever any hyperhive service runs, the hive or a swarm
# service alone. The host enabling nothing is the control that it is
# gated at all, and has to pass every assertion: it is what an
# operator gets from importing the module.
name = "a missing swarm.domain is refused on a hive and on a swarm-service host, and a host enabling nothing evaluates";
ok =
let
noDomain = extra: hive ({ swarm.domain = null; } // extra);
refusedSwarmDomain =
extra:
lib.any (
a: !a.assertion && lib.hasInfix "services.hyperhive.swarm.domain to be set" a.message
) (noDomain extra).assertions;
in
refusedSwarmDomain { }
&& refusedSwarmDomain {
deploy.hive-controller.enable = false;
deploy.nats.enable = true;
}
&& lib.all (a: a.assertion) (noDomain { deploy.hive-controller.enable = false; }).assertions;
}
]; ];
in in
runGroup "core-toggle" cases runGroup "core-toggle" cases