diff --git a/docs/swarm/README.md b/docs/swarm/README.md index 7ace9665..455cd5bb 100644 --- a/docs/swarm/README.md +++ b/docs/swarm/README.md @@ -310,8 +310,9 @@ services.hyperhive = { 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 -hive, the secret store or the homeserver. Eval fails with a hint naming +**required** on every host that runs any hyperhive service; a host that +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 that evaluates cleanly and deploys. diff --git a/nix/host-modules/hive-network.nix b/nix/host-modules/hive-network.nix index 24c601ca..73f7a86e 100644 --- a/nix/host-modules/hive-network.nix +++ b/nix/host-modules/hive-network.nix @@ -27,6 +27,38 @@ let hostCount = pow2 (32 - cfg.bridgePrefixLength); # Mask off host bits to get the network base address. 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 { # 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 - # running no hive and no swarm service needs the value too. The - # `.invalid` fallbacks described above keep this message reachable. - { + # Gated on any hyperhive service, not only the hive: the swarm's options + # are identical on every host, so a host running only a swarm service + # needs the value too. The `.invalid` fallbacks described above keep + # this message reachable. + (lib.mkIf anyServiceEnabled { assertions = [ { assertion = config.services.hyperhive.swarm.domain != null; message = '' hyperhive requires services.hyperhive.swarm.domain to be set - on every host, to the same value on every host of the swarm — - the DNS domain of the swarm, of which each hive occupies one - sub-domain. A guessed value would be a wrong hostname that - evaluates cleanly and deploys. Set it + on every host that runs a hyperhive service, to the same value + on every host of the swarm — the DNS domain of the swarm, of + which each hive occupies one sub-domain. A guessed value would + be a wrong hostname that evaluates cleanly and deploys. Set it (`services.hyperhive.swarm.domain = "example.com";`) — with `hiveName` it also derives `services.hyperhive.domain` for you. ''; } ]; - } + }) # The bridge itself, up only where something hangs off it. Private # netns is still the only container mode. diff --git a/nix/host-modules/hyperhive.nix b/nix/host-modules/hyperhive.nix index 9c6bedd0..b68540eb 100644 --- a/nix/host-modules/hyperhive.nix +++ b/nix/host-modules/hyperhive.nix @@ -117,8 +117,10 @@ in `services.hyperhive.hiveName`, list the hives by name, and no hive in the swarm states an address at all. - **Required** on every host that imports this module, with the - same value on every host of the swarm, and deliberately not + **Required** on every host that runs a hyperhive service (a + `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 evaluates cleanly and deploys, which is worse than an eval failure telling an operator to write down the one address their diff --git a/nix/module-eval/core-toggle.nix b/nix/module-eval/core-toggle.nix index 54b38fda..4ec25819 100644 --- a/nix/module-eval/core-toggle.nix +++ b/nix/module-eval/core-toggle.nix @@ -503,6 +503,28 @@ let } && !(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 runGroup "core-toggle" cases