hyperhive/nix/module-eval/swarm-services-switch.nix
atlas 3202cde704 nix: gate hive-c0re on deploy.hive-controller.enable, drop hyperhive.enable
`services.hyperhive.enable` and `services.hyperhive.c0re.enable` are gone.
One switch, `services.hyperhive.deploy.hive-controller.enable` (default
false, as the old toggle was), now gates hive-c0re and hive-priv. Both old
paths are `mkRenamedOptionModule` shims in deploy.nix, so a host config
that still sets either evaluates as before and gets a rename warning.

Every other read of the old toggle is resolved, including the 29 made
through the `hyperhiveCfg`/`hiveCfg` aliases:

- Dropped: each swarm service and its glue keeps only its own deploy
  toggle (authelia, bao and its PKI glue, grafana, victorialogs,
  victoriametrics, the secret publisher, swarm-ca, the OIDC client rows,
  the controller/nats/matrix-ctl/publisher/services-issuer identities),
  the forge, and the `domain` deprecation warning.
- To deploy.hive-controller.enable: the queue-agent credential reader and
  its assertion, which feed hive-c0re and write under its state dir, plus
  their policy-order entry; the network identity assertions; hive-tls's
  two writes into hive-c0re's environment.
- hive-tls runs where the gateway runs self-signed
  (`gateway.enable && useSelfSigned`), not on every host.
- The matrix appservice-token reader and its assertion stay on
  `deploy.matrix.enable` plus their client-identity checks. They read
  deploy.matrix's token file and registration script; their deploy.bao
  inputs are the client-half options a hive sets to read a store it does
  not run, so gating on deploy.bao.enable would drop the tested
  remote-reader case.
- The `hiveName` assertion moves from hive-network.nix to hyperhive.nix
  and fires wherever the hive, the store or the homeserver runs: each
  turns the name into an identifier with no fallback.

On a host with `deploy.allSwarmServices` and no hive, the documented
services-host recipe, authelia, bao, grafana, victorialogs,
victoriametrics, the OIDC client rows and the hive CA now render; before,
the old toggle being off left them out.

Refs #4500
2026-09-26 01:19:49 +02:00

219 lines
8.5 KiB
Nix

# `checks.module-eval-swarm-services-switch` — see ./lib.nix for the shared
# rationale (why this suite exists, naming convention, "evaluates
# not executes").
{
pkgs,
lib,
self,
nixosSystem,
}:
let
inherit
(import ./lib.nix {
inherit
pkgs
lib
self
nixosSystem
;
})
hive
runGroup
bridgePortsOrNone
swarmServiceEnables
;
# A dedicated services host with hives elsewhere: the swarm-services
# toggle and no hive, the recipe docs/swarm/services.md gives.
swarmServicesOnly = hive {
deploy.hive-controller.enable = false;
deploy.allSwarmServices = true;
};
# The same switch with every service it owns placed elsewhere, so the only
# thing that can enable the gateway/resolver/bridge here is that toggle's
# own module — each service module asserts them from its own `enable`.
swarmServicesSwitchAlone = hive {
deploy.hive-controller.enable = false;
deploy.allSwarmServices = true;
deploy.matrix.enable = false;
otel.enable = false;
deploy.authelia.enable = false;
deploy.nats.enable = false;
deploy.swarm-otel.enable = false;
deploy.victoriametrics.enable = false;
deploy.grafana.enable = false;
deploy.victorialogs.enable = false;
deploy.bao.enable = false;
deploy.forgejo.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 {
deploy.hive-controller.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;
};
# 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 swarm-wide forge objects are the controller's (forge/objects.rs), so
# the mirror list and the org avatar go to its unit, not hive-c0re's.
aMirror = {
upstream = "https://example.invalid/tool";
dest = "mirrors/tool";
};
controllerWithMirrors = hive {
deploy.swarm-controller.enable = true;
deploy.forgejo.enable = true;
deploy.forgejo.mirrors = [ aMirror ];
c0re.orgAvatarPng = "/etc/marker-avatar.png";
};
mirrorsNoController = hive {
deploy.forgejo.enable = true;
deploy.forgejo.mirrors = [ aMirror ];
};
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);
}
{
# 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;
}
{
# 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);
}
{
name = "the swarm UI claims the swarm apex where this host serves it";
ok = swarmUiHere.services.nginx.virtualHosts ? "t.local";
}
{
# The swarm-services toggle enables them explicitly, from its own
# module rather than from any of their defaults.
name = "the swarm-services toggle turns on the gateway, resolver and bridge by itself";
ok =
swarmServicesSwitchAlone.services.hyperhive.gateway.enable
&& swarmServicesSwitchAlone.services.hyperhive.gateway.dns.enable
&& swarmServicesSwitchAlone.services.hyperhive.network.enable;
}
{
# Each service is gated on its own deployment toggle alone, so a host
# that runs no hive still runs every shared service the switch turns
# on, plus the plumbing and the self-signed CA they are served through.
# Nothing starts the hive's own daemons. `hive-c0re` is checked by what
# starts it rather than by key: ../host-modules/hive-network.nix's bridge
# block writes to its environment wherever the bridge is up.
name = "a services-only host runs the swarm's shared services and no hive";
ok =
let
c = swarmServicesOnly.containers;
s = swarmServicesOnly.systemd.services;
h = swarmServicesOnly.services.hyperhive;
in
lib.all (m: c ? ${m}) [
"swarm-authelia"
"swarm-bao"
"swarm-grafana"
"swarm-victorialogs"
"swarm-victoriametrics"
"swarm-otel"
"swarm-nats"
"hive-matrix"
"hive-forge"
]
&& h.gateway.enable
&& h.gateway.dns.enable
&& h.network.enable
&& s ? hive-tls-ca
&& s ? swarm-bao-pki
&& (s.hive-c0re.wantedBy or [ ]) == [ ]
&& !(swarmServicesOnly.systemd.sockets ? hive-c0re)
&& !(swarmServicesOnly.systemd.sockets ? hive-priv)
&& !(s ? swarm-bao-queue-agent);
}
{
# An operator's explicit `false` beats every `mkDefault` assertion,
# which is what keeps "asserted by whoever needs it" from being a
# setting the operator cannot turn off.
name = "an explicit gateway.enable = false wins over the modules asserting it";
ok = !(hive { gateway.enable = false; }).services.nginx.enable;
}
{
name = "declared forge mirrors reach the swarm-controller, not hive-c0re";
ok =
builtins.fromJSON controllerWithMirrors.systemd.services.swarm-controller.environment.SWARM_CONTROLLER_FORGE_MIRRORS
== [ aMirror ]
&& !(controllerWithMirrors.systemd.services.hive-c0re.environment ? HYPERHIVE_FORGE_MIRRORS);
}
{
name = "forge mirrors on a host without the controller warn that nothing seeds them";
ok =
let
warns = cfg: lib.any (lib.hasInfix "declare them on the swarm-controller's host") cfg.warnings;
in
warns mirrorsNoController && !(warns controllerWithMirrors);
}
{
name = "the old c0re org-avatar option lands on the controller's unit";
ok =
controllerWithMirrors.systemd.services.swarm-controller.environment.SWARM_CONTROLLER_CONFIG_ORG_AVATAR_PNG
== "/etc/marker-avatar.png"
&& !(controllerWithMirrors.systemd.services.hive-c0re.environment ? HIVE_ORG_AVATAR_PNG);
}
];
in
runGroup "swarm-services-switch" cases