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:
parent
eded8f2e4e
commit
fb2fff0668
4 changed files with 71 additions and 13 deletions
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue