diff --git a/nix/module-eval.nix b/nix/module-eval.nix index 71931666..91130618 100644 --- a/nix/module-eval.nix +++ b/nix/module-eval.nix @@ -453,8 +453,85 @@ let in builtins.foldl' (acc: v: builtins.seq v acc) true vals; + # The swarm's shared services hosted HERE without the all-local mode — a + # services box with hives elsewhere, the shape ./host-modules/ + # swarm-required-services.nix documents the switch for. + swarmServicesHere = hive { deploy.allSwarmServices = true; }; + + # Same, with one of those services placed on another host. Every derivation + # in that module is `mkDefault` so this stays expressible. + swarmServicesBaoElsewhere = hive { + deploy.allSwarmServices = true; + deploy.bao.enable = false; + }; + + # The enables that switch owns. ⚠️ `otel` is the per-hive collector's own + # option and is NOT under `deploy` — spelled at the wrong path it would be + # undeclared rather than false, and a roster that quietly loses a member is + # what the count guard in the cases below exists to catch. Its membership + # here is deliberate and was the fix for a gap, not an oversight: the hive + # tier lands wherever the swarm services do. + swarmServiceEnables = + machine: + let + h = machine.services.hyperhive; + in + { + matrix = h.deploy.matrix.enable; + otel = h.otel.enable; + authelia = h.deploy.authelia.enable; + nats = h.deploy.nats.enable; + swarm-otel = h.deploy.swarm-otel.enable; + victoriametrics = h.deploy.victoriametrics.enable; + grafana = h.deploy.grafana.enable; + victorialogs = h.deploy.victorialogs.enable; + bao = h.deploy.bao.enable; + }; + # Each case: a name stating the property, and `ok`. cases = [ + { + # `lib.all` over an empty set holds vacuously, so the roster is counted + # before it is read: a helper that lost a member would otherwise turn + # this case green by measuring nothing. + name = "hosting the swarm's shared services turns on every service that switch owns"; + ok = + let + es = swarmServiceEnables swarmServicesHere; + in + lib.length (lib.attrNames es) == 9 && lib.all lib.id (lib.attrValues es); + } + { + name = "a hive that does not host the swarm's shared services runs none of them"; + ok = + let + es = swarmServiceEnables bare; + in + lib.length (lib.attrNames es) == 9 && !lib.any lib.id (lib.attrValues es); + } + { + # The switch fills in for an operator who has not spoken and yields to + # one who has — that is what keeps a shared service placeable on a host + # of its own. A plain assignment or `mkForce` would satisfy both cases + # above and break this one. `nats` is the control: without it the case + # also passes on a fixture where nothing came on at all. + name = "placing one shared service elsewhere survives the switch that would enable it"; + ok = + let + es = swarmServiceEnables swarmServicesBaoElsewhere; + in + !es.bao && es.nats; + } + { + # The controller sits on the OTHER tier: `singleHostSwarm` places it + # (./host-modules/local-defaults.nix) and swarm-ui follows the + # controller. Pinned so that moving a service between tiers is a + # decision someone makes rather than a merge nobody reads. + name = "hosting the swarm's shared services does not make a hive the swarm's control plane"; + ok = + !swarmServicesHere.services.hyperhive.deploy.swarm-controller.enable + && !swarmServicesHere.services.hyperhive.deploy.swarm-ui.enable; + } { name = "a hive that has not opted into all-local runs no swarm controller"; ok = !bare.services.hyperhive.deploy.swarm-controller.enable;