From 3202cde7042641772c329c39d5f241130bfbce4c Mon Sep 17 00:00:00 2001 From: atlas Date: Sat, 26 Sep 2026 00:32:32 +0200 Subject: [PATCH] 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 --- README.md | 2 +- docs/networking/network.md | 2 +- docs/networking/snapshot-store.md | 2 +- docs/swarm/README.md | 6 +- flake.nix | 2 +- nix/docs/default.nix | 2 +- nix/host-modules/default.nix | 5 +- nix/host-modules/deploy.nix | 36 +++++++-- .../glue-bao-readers-policy-order.nix | 9 +-- nix/host-modules/glue-bao-tls.nix | 9 ++- .../glue-controller-bao-identity.nix | 2 +- nix/host-modules/glue-forge-oidc-client.nix | 2 +- nix/host-modules/glue-grafana-oidc-client.nix | 2 +- nix/host-modules/glue-matrix-bao-token.nix | 7 +- .../glue-matrix-ctl-bao-identity.nix | 2 +- nix/host-modules/glue-nats-bao-identity.nix | 2 +- .../glue-queue-agent-credential.nix | 23 +++--- .../glue-secret-publisher-bao-identity.nix | 14 ++-- .../glue-services-issuer-bao-identity.nix | 2 +- .../glue-swarm-bao-otel-oidc-client.nix | 2 +- .../glue-swarm-otel-oidc-client.nix | 2 +- nix/host-modules/hive-c0re/default.nix | 4 +- nix/host-modules/hive-c0re/options.nix | 7 -- nix/host-modules/hive-forge/default.nix | 2 +- nix/host-modules/hive-matrix.nix | 3 +- nix/host-modules/hive-network.nix | 19 ++--- nix/host-modules/hive-priv.nix | 2 +- nix/host-modules/hive-tls.nix | 37 +++++----- nix/host-modules/hyperhive.nix | 74 ++++++++++++------- nix/host-modules/swarm-authelia.nix | 2 +- nix/host-modules/swarm-bao.nix | 6 +- nix/host-modules/swarm-ca.nix | 2 +- nix/host-modules/swarm-controller.nix | 2 +- nix/host-modules/swarm-grafana.nix | 2 +- nix/host-modules/swarm-secret-publisher.nix | 2 +- nix/host-modules/swarm-victorialogs.nix | 2 +- nix/host-modules/swarm-victoriametrics.nix | 2 +- nix/host-modules/swarm-wireguard.nix | 12 ++- nix/module-eval/bao-matrix-reader.nix | 3 +- nix/module-eval/core-toggle.nix | 61 +++++++++++++-- nix/module-eval/lib.nix | 2 +- nix/module-eval/swarm-services-switch.nix | 68 +++++++++++++++-- swarm-controller/README.md | 5 +- 43 files changed, 292 insertions(+), 162 deletions(-) diff --git a/README.md b/README.md index b6bb21af..68c5a5eb 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ Minimal `flake.nix` for a host that runs hive-c0re: modules = [ hyperhive.nixosModules.default # hive-c0re + hive-forge + hive-gateway in one import ({ ... }: { - services.hyperhive.enable = true; + services.hyperhive.deploy.hive-controller.enable = true; # services.hyperhive.c0re.operatorPronouns = "they/them"; # default: "she/her" # ... rest of your host config diff --git a/docs/networking/network.md b/docs/networking/network.md index 1efcf70a..38527cc9 100644 --- a/docs/networking/network.md +++ b/docs/networking/network.md @@ -90,7 +90,7 @@ listener on `bridgeIp` is on the host's bridge interface. ```nix { services.hyperhive = { - enable = true; + deploy.hive-controller.enable = true; hiveName = "pr1ma"; swarm.domain = "darkest.space"; swarm.hives.pr1ma = { }; # -> domain = pr1ma.darkest.space diff --git a/docs/networking/snapshot-store.md b/docs/networking/snapshot-store.md index 2eb733db..9e3353ee 100644 --- a/docs/networking/snapshot-store.md +++ b/docs/networking/snapshot-store.md @@ -38,7 +38,7 @@ The store host is a swarm member like any other: it gets an entry in [swarm/](../swarm/README.md) for the mesh itself. Note that `deploy.wireguard.enable` gates the mesh, **not** -`c0re.enable` --- a store host runs no hive and would otherwise get no +`deploy.hive-controller.enable` --- a store host runs no hive and would otherwise get no `wg-hive` interface at all. ## Pointing a hive at it diff --git a/docs/swarm/README.md b/docs/swarm/README.md index 57c96a20..37808df0 100644 --- a/docs/swarm/README.md +++ b/docs/swarm/README.md @@ -299,9 +299,9 @@ migrating agent keeps one unbroken incremental chain. See `services.hyperhive.deploy.swarm-controller.enable` runs the `swarm-controller` daemon on this host. **Off by default and deliberately not derived from -`services.hyperhive.enable`**: a swarm has one controller, so enabling it -is a statement about swarm topology, not about whether the operator has -installed hyperhive. Every hive runs `hive-c0re` (the agents on that host); one +`services.hyperhive.deploy.hive-controller.enable`**: a swarm has one +controller, so enabling it states a fact about swarm topology, not about +whether this host runs a hive. Every hive runs `hive-c0re` (the agents on that host); one hive additionally runs this (what's true across hives). What it serves, why it's a unix socket rather than a port, and the diff --git a/flake.nix b/flake.nix index 3171895d..b8c4dd88 100644 --- a/flake.nix +++ b/flake.nix @@ -119,7 +119,7 @@ # an operator override still wins. Intended usage: # # imports = [ hyperhive.nixosModules.default ]; - # services.hyperhive.enable = true; + # services.hyperhive.deploy.hive-controller.enable = true; # default = { lib, pkgs, ... }: diff --git a/nix/docs/default.nix b/nix/docs/default.nix index 65dbfa56..2c76c719 100644 --- a/nix/docs/default.nix +++ b/nix/docs/default.nix @@ -54,7 +54,7 @@ let }; boot.loader.grub.enable = false; system.stateVersion = "25.11"; - services.hyperhive.enable = lib.mkForce false; + services.hyperhive.deploy.hive-controller.enable = lib.mkForce false; services.hyperhive.deploy.matrix.enable = lib.mkForce false; } ) diff --git a/nix/host-modules/default.nix b/nix/host-modules/default.nix index 335ab66f..4bcb348d 100644 --- a/nix/host-modules/default.nix +++ b/nix/host-modules/default.nix @@ -1,7 +1,8 @@ # The full hyperhive host stack, pulled together in one place — this # is what the flake exports as `nixosModules.default` (wrapped with # the package/source wiring; see flake.nix). One import covers -# everything; `services.hyperhive.enable = true` turns the stack on. +# everything; `services.hyperhive.deploy.hive-controller.enable = true` +# runs a hive on this host. # # Every swarm needs the forge — hive-c0re mirrors every agent's applied # config repo into it and it's the canonical store for the meta flake @@ -9,7 +10,7 @@ # runs it (derived from `deploy.allSwarmServices`, see # ./swarm-required-services.nix). hive-matrix is opt-in (off by default). All # subsystems rely on `services.hyperhive.domain`, which is required -# (asserted in hive-network.nix) whenever hyperhive is enabled. +# (asserted in hive-network.nix) wherever this host runs a hive. { imports = [ ./hyperhive.nix diff --git a/nix/host-modules/deploy.nix b/nix/host-modules/deploy.nix index 30c228a2..34bd3f3c 100644 --- a/nix/host-modules/deploy.nix +++ b/nix/host-modules/deploy.nix @@ -119,6 +119,15 @@ in [ "services" "hyperhive" "swarm" "statusPublish" "clientSecretFile" ] [ "services" "hyperhive" "deploy" "hive-controller" "statusPublish" "clientSecretFile" ] ) + # Both old spellings of "run a hive here" forward to the one switch. + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "enable" ] + [ "services" "hyperhive" "deploy" "hive-controller" "enable" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "c0re" "enable" ] + [ "services" "hyperhive" "deploy" "hive-controller" "enable" ] + ) (lib.mkRenamedOptionModule [ "services" "hyperhive" "swarm" "otel" "enable" ] [ "services" "hyperhive" "deploy" "swarm-otel" "enable" ] @@ -452,9 +461,9 @@ in Run the swarm's metrics UI on this host. Off by default and not derived from - {option}`services.hyperhive.enable`: a swarm has one Grafana, so - running it is a decision about this host rather than about - whether hyperhive is installed. + {option}`services.hyperhive.deploy.hive-controller.enable`: a swarm + has one Grafana, so running it is a decision about this host rather + than about whether it runs a hive. ''; }; @@ -570,6 +579,21 @@ in ''; }; + hive-controller.enable = lib.mkOption { + type = lib.types.bool; + default = false; + example = true; + description = '' + Run a hive on this host: the `hive-c0re` coordinator that owns every + agent container here, its `hive-priv` helper, and the bridge, + resolver and gateway the agents hang off. + + Every hive turns this on. A host that only runs the swarm's shared + services ({option}`services.hyperhive.deploy.allSwarmServices`) + leaves it off. + ''; + }; + swarm-controller.enable = lib.mkOption { type = lib.types.bool; default = false; @@ -577,9 +601,9 @@ in Run the swarm-controller daemon on this host. Off by default and deliberately not derived from - {option}`services.hyperhive.enable`: a swarm has one controller, - so running it is a decision about this host rather than about - whether hyperhive is installed. + {option}`services.hyperhive.deploy.hive-controller.enable`: a swarm + has one controller, so running it is a decision about this host + rather than about whether it runs a hive. ''; }; diff --git a/nix/host-modules/glue-bao-readers-policy-order.nix b/nix/host-modules/glue-bao-readers-policy-order.nix index 16cfe5d5..018eae19 100644 --- a/nix/host-modules/glue-bao-readers-policy-order.nix +++ b/nix/host-modules/glue-bao-readers-policy-order.nix @@ -31,22 +31,21 @@ let readersHere = { # ./glue-matrix-bao-token.nix swarm-bao-matrix-token = - hyperhiveCfg.enable - && havePair baoDeploy.matrixTokenClientCertFile baoDeploy.matrixTokenClientKeyFile + havePair baoDeploy.matrixTokenClientCertFile baoDeploy.matrixTokenClientKeyFile && deployCfg.matrix.enable; # ./glue-queue-agent-credential.nix swarm-bao-queue-agent = - hyperhiveCfg.enable + deployCfg.hive-controller.enable && havePair baoDeploy.queueAgentClientCertFile baoDeploy.queueAgentClientKeyFile; # ./swarm-grafana.nix - swarm-bao-grafana-oidc = hyperhiveCfg.enable && deployCfg.grafana.enable; + swarm-bao-grafana-oidc = deployCfg.grafana.enable; # ./swarm-otel.nix swarm-bao-otel-oidc = deployCfg.swarm-otel.enable && havePair baoDeploy.otelOidcClientCertFile baoDeploy.otelOidcClientKeyFile; # ./swarm-bao.nix: its block is gated on the store, which `orderAfterPolicy` # already checks. - swarm-bao-forwarder-oidc = hyperhiveCfg.enable; + swarm-bao-forwarder-oidc = true; # ./swarm-nats.nix: the queue's TLS leaf, not a secret, but the same wait. swarm-bao-nats-tls = deployCfg.nats.enable; }; diff --git a/nix/host-modules/glue-bao-tls.nix b/nix/host-modules/glue-bao-tls.nix index 9073c293..4201d3b1 100644 --- a/nix/host-modules/glue-bao-tls.nix +++ b/nix/host-modules/glue-bao-tls.nix @@ -43,9 +43,10 @@ let # What a reader calls itself to the store. The hive's name, because a bao # cert-auth role matches on the CN — this is an interface, not a label. - # No fallback: `hiveName` is asserted set for every hyperhive host, which is - # the same condition this file's `config` is gated on. A fallback here reads - # as a second supported spelling and there is no such thing. + # No fallback: `hiveName` is asserted set wherever `deploy.bao.enable` is + # (./hyperhive.nix), the same condition this file's `config` is gated on. A + # fallback here reads as a second supported spelling and there is no such + # thing. clientCn = hyperhiveCfg.hiveName; # $1 dir $2 basename $3 CN $4 SAN or "" $5 EKU @@ -71,7 +72,7 @@ let ''; in { - config = lib.mkIf (hyperhiveCfg.enable && deployCfg.bao.enable) { + config = lib.mkIf deployCfg.bao.enable { services.hyperhive.deploy.bao = { serverCertFile = lib.mkDefault "${pkiDir}/server.pem"; serverKeyFile = lib.mkDefault "${pkiDir}/server-key.pem"; diff --git a/nix/host-modules/glue-controller-bao-identity.nix b/nix/host-modules/glue-controller-bao-identity.nix index 555102f3..86fba6d5 100644 --- a/nix/host-modules/glue-controller-bao-identity.nix +++ b/nix/host-modules/glue-controller-bao-identity.nix @@ -32,7 +32,7 @@ let pkiDir = if haveMintedPki then builtins.dirOf baoDeploy.clientCertFile else null; in { - config = lib.mkIf (hyperhiveCfg.enable && deployCfg.swarm-controller.enable && haveMintedPki) { + config = lib.mkIf (deployCfg.swarm-controller.enable && haveMintedPki) { services.hyperhive.deploy.swarm-controller = { baoClientCertFile = lib.mkDefault "${pkiDir}/controller.pem"; baoClientKeyFile = lib.mkDefault "${pkiDir}/controller-key.pem"; diff --git a/nix/host-modules/glue-forge-oidc-client.nix b/nix/host-modules/glue-forge-oidc-client.nix index 6e9ca92b..51789a06 100644 --- a/nix/host-modules/glue-forge-oidc-client.nix +++ b/nix/host-modules/glue-forge-oidc-client.nix @@ -24,7 +24,7 @@ let forgeCfg = hyperhiveCfg.swarm.forge; in { - config = lib.mkIf (hyperhiveCfg.enable && deployCfg.authelia.enable) { + config = lib.mkIf deployCfg.authelia.enable { # One declaration, two readers. The forge knows its own callback URL; # making the operator restate it in authelia's client list would be a # second source of truth for a string whose mismatch is a silent diff --git a/nix/host-modules/glue-grafana-oidc-client.nix b/nix/host-modules/glue-grafana-oidc-client.nix index 69515e28..15884847 100644 --- a/nix/host-modules/glue-grafana-oidc-client.nix +++ b/nix/host-modules/glue-grafana-oidc-client.nix @@ -32,7 +32,7 @@ let grafanaCfg = hyperhiveCfg.swarm.grafana; in { - config = lib.mkIf (hyperhiveCfg.enable && deployCfg.authelia.enable) { + config = lib.mkIf deployCfg.authelia.enable { # One declaration, two readers. Grafana's callback URL is format-locked to # its own root URL, and the read-only option it is taken from is where that # format is spelled — restating it here would be a second source of truth diff --git a/nix/host-modules/glue-matrix-bao-token.nix b/nix/host-modules/glue-matrix-bao-token.nix index 3c0afcb6..3c1bdb62 100644 --- a/nix/host-modules/glue-matrix-bao-token.nix +++ b/nix/host-modules/glue-matrix-bao-token.nix @@ -69,7 +69,8 @@ let # pieces; this literal is the nix half of that one agreement. # # `hiveName` has no fallback here for the reason ./glue-bao-tls.nix gives at - # its own use of it: it is asserted set for every hyperhive host. + # its own use of it: it is asserted set on every host that runs the + # homeserver. tokenPath = "secret/swarm/hives/${hyperhiveCfg.hiveName}/matrix/appservice-token"; # A literal, not an option — ./hive-matrix.nix names its container @@ -94,7 +95,7 @@ in # is a hive that has no store, which is supported. So this one additionally # requires `hiveReaderIdentity`: the host demonstrably reads the store, and # named every option but this pair. - (lib.mkIf (hyperhiveCfg.enable && deployCfg.matrix.enable && hiveReaderIdentity) { + (lib.mkIf (deployCfg.matrix.enable && hiveReaderIdentity) { assertions = [ { assertion = haveClientIdentity; @@ -124,7 +125,7 @@ in ]; }) - (lib.mkIf (hyperhiveCfg.enable && haveClientIdentity && deployCfg.matrix.enable) { + (lib.mkIf (haveClientIdentity && deployCfg.matrix.enable) { # Same rule as the unit's own gate: this reader exists on a host that has a # client identity and a homeserver, which is not every host that runs the # store, so the store's module cannot name it. diff --git a/nix/host-modules/glue-matrix-ctl-bao-identity.nix b/nix/host-modules/glue-matrix-ctl-bao-identity.nix index 0d725ad4..ac7cfda8 100644 --- a/nix/host-modules/glue-matrix-ctl-bao-identity.nix +++ b/nix/host-modules/glue-matrix-ctl-bao-identity.nix @@ -31,7 +31,7 @@ let pkiDir = if haveMintedPki then builtins.dirOf baoDeploy.clientCertFile else null; in { - config = lib.mkIf (hyperhiveCfg.enable && deployCfg.matrix.enable && haveMintedPki) { + config = lib.mkIf (deployCfg.matrix.enable && haveMintedPki) { services.hyperhive.deploy.matrix = { ctlBaoClientCertFile = lib.mkDefault "${pkiDir}/matrix-ctl.pem"; ctlBaoClientKeyFile = lib.mkDefault "${pkiDir}/matrix-ctl-key.pem"; diff --git a/nix/host-modules/glue-nats-bao-identity.nix b/nix/host-modules/glue-nats-bao-identity.nix index 497a5cb7..6daaddde 100644 --- a/nix/host-modules/glue-nats-bao-identity.nix +++ b/nix/host-modules/glue-nats-bao-identity.nix @@ -31,7 +31,7 @@ let pkiDir = if haveMintedPki then builtins.dirOf baoDeploy.clientCertFile else null; in { - config = lib.mkIf (hyperhiveCfg.enable && deployCfg.nats.enable && haveMintedPki) { + config = lib.mkIf (deployCfg.nats.enable && haveMintedPki) { services.hyperhive.deploy.nats = { baoClientCertFile = lib.mkDefault "${pkiDir}/nats.pem"; baoClientKeyFile = lib.mkDefault "${pkiDir}/nats-key.pem"; diff --git a/nix/host-modules/glue-queue-agent-credential.nix b/nix/host-modules/glue-queue-agent-credential.nix index 8fa2dc06..21900692 100644 --- a/nix/host-modules/glue-queue-agent-credential.nix +++ b/nix/host-modules/glue-queue-agent-credential.nix @@ -20,10 +20,10 @@ # into one that is refused, which reaches the agent as a timeout. # # 📌 This runs wherever a client identity is configured, NOT only where the -# store is — the rule ./glue-matrix-bao-token.nix states in full. There is no -# "this hive runs agents" condition to gate it on as well: agent containers -# are created at runtime by hive-c0re, so every hyperhive host is a host that -# may run one. +# store is — the rule ./glue-matrix-bao-token.nix states in full — and +# wherever this host runs a hive (`deploy.hive-controller.enable`): the +# credential is for the agents hive-c0re creates here at runtime, and the unit +# writes under hive-c0re's own state directory. { pkgs, lib, @@ -74,7 +74,8 @@ let # one agreement. # # `hiveName` has no fallback here for the reason ./glue-bao-tls.nix gives at - # its own use of it: it is asserted set for every hyperhive host. + # its own use of it: it is asserted set wherever + # `deploy.hive-controller.enable` is. credentialPath = "secret/swarm/hives/${hyperhiveCfg.hiveName}/queue/agent"; in { @@ -110,11 +111,11 @@ in # # Shaped after ./swarm-grafana.nix's `haveClientIdentity` assertion — the # same refusal, named to this principal's own pair. Where Grafana's gate - # is `deploy.grafana.enable`, this reader has no toggle of its own to - # check: every hive runs one for its own agents, so reading the store at - # all is what asks for it. Hence `hiveReaderIdentity` alone — a host - # holding no hive leaf has no store to read and nothing is missing. - (lib.mkIf (hyperhiveCfg.enable && hiveReaderIdentity) { + # is `deploy.grafana.enable`, this reader's is the hive's own: every hive + # runs one for its own agents, so on a hive, reading the store at all is + # what asks for it. Hence `hiveReaderIdentity` — a host holding no hive + # leaf has no store to read and nothing is missing. + (lib.mkIf (deployCfg.hive-controller.enable && hiveReaderIdentity) { assertions = [ { assertion = haveClientIdentity; @@ -149,7 +150,7 @@ in ]; }) - (lib.mkIf (hyperhiveCfg.enable && haveClientIdentity) { + (lib.mkIf (deployCfg.hive-controller.enable && haveClientIdentity) { # Same rule as the unit's own gate: this reader exists on any host holding # a client identity, which is not every host that runs the store, so the # store's module cannot name it. diff --git a/nix/host-modules/glue-secret-publisher-bao-identity.nix b/nix/host-modules/glue-secret-publisher-bao-identity.nix index 88fc92b9..52e0f3cf 100644 --- a/nix/host-modules/glue-secret-publisher-bao-identity.nix +++ b/nix/host-modules/glue-secret-publisher-bao-identity.nix @@ -31,12 +31,10 @@ let pkiDir = if haveMintedPki then builtins.dirOf baoDeploy.clientCertFile else null; in { - config = - lib.mkIf (hyperhiveCfg.enable && deployCfg.swarm-secret-publisher.enable && haveMintedPki) - { - services.hyperhive.deploy.swarm-secret-publisher = { - baoClientCertFile = lib.mkDefault "${pkiDir}/secret-publisher.pem"; - baoClientKeyFile = lib.mkDefault "${pkiDir}/secret-publisher-key.pem"; - }; - }; + config = lib.mkIf (deployCfg.swarm-secret-publisher.enable && haveMintedPki) { + services.hyperhive.deploy.swarm-secret-publisher = { + baoClientCertFile = lib.mkDefault "${pkiDir}/secret-publisher.pem"; + baoClientKeyFile = lib.mkDefault "${pkiDir}/secret-publisher-key.pem"; + }; + }; } diff --git a/nix/host-modules/glue-services-issuer-bao-identity.nix b/nix/host-modules/glue-services-issuer-bao-identity.nix index 44298c0e..32f28f3f 100644 --- a/nix/host-modules/glue-services-issuer-bao-identity.nix +++ b/nix/host-modules/glue-services-issuer-bao-identity.nix @@ -34,7 +34,7 @@ let pkiDir = if haveMintedPki then builtins.dirOf baoDeploy.clientCertFile else null; in { - config = lib.mkIf (hyperhiveCfg.enable && haveMintedPki) { + config = lib.mkIf haveMintedPki { services.hyperhive.deploy.hive-controller.tls = { baoClientCertFile = lib.mkDefault "${pkiDir}/services-issuer.pem"; baoClientKeyFile = lib.mkDefault "${pkiDir}/services-issuer-key.pem"; diff --git a/nix/host-modules/glue-swarm-bao-otel-oidc-client.nix b/nix/host-modules/glue-swarm-bao-otel-oidc-client.nix index 6e9d850a..b059bf30 100644 --- a/nix/host-modules/glue-swarm-bao-otel-oidc-client.nix +++ b/nix/host-modules/glue-swarm-bao-otel-oidc-client.nix @@ -29,7 +29,7 @@ let baoCfg = hyperhiveCfg.swarm.bao; in { - config = lib.mkIf (hyperhiveCfg.enable && deployCfg.authelia.enable) { + config = lib.mkIf deployCfg.authelia.enable { # One declaration, two readers: `clientId` is a read-only option # ./swarm-bao.nix owns, and ./swarm-otel.nix builds this principal's # authenticator audience from the same option. diff --git a/nix/host-modules/glue-swarm-otel-oidc-client.nix b/nix/host-modules/glue-swarm-otel-oidc-client.nix index a10eee4b..d38303e4 100644 --- a/nix/host-modules/glue-swarm-otel-oidc-client.nix +++ b/nix/host-modules/glue-swarm-otel-oidc-client.nix @@ -32,7 +32,7 @@ let otelCfg = hyperhiveCfg.swarm.otel; in { - config = lib.mkIf (hyperhiveCfg.enable && deployCfg.authelia.enable) { + config = lib.mkIf deployCfg.authelia.enable { # One declaration, two readers: `clientId` and `audience` are read-only # options ./swarm-otel.nix derives from the scrape/push targets it owns, # so this file states neither formula a second time. diff --git a/nix/host-modules/hive-c0re/default.nix b/nix/host-modules/hive-c0re/default.nix index 52f1ce2f..840d3535 100644 --- a/nix/host-modules/hive-c0re/default.nix +++ b/nix/host-modules/hive-c0re/default.nix @@ -153,11 +153,11 @@ in name = "hive-c0re"; consumers = [ "hive-c0re" ]; hostUnit = true; - enable = cfg.enable; + enable = config.services.hyperhive.deploy.hive-controller.enable; }) ]; - config = lib.mkIf cfg.enable { + config = lib.mkIf config.services.hyperhive.deploy.hive-controller.enable { # This module is the host's only knowledge that agent containers # exist: they hang off the bridge, resolve through dnsmasq, and their # UIs — plus the operator dashboard — are served by the gateway. diff --git a/nix/host-modules/hive-c0re/options.nix b/nix/host-modules/hive-c0re/options.nix index 8c97fa51..e074bb6f 100644 --- a/nix/host-modules/hive-c0re/options.nix +++ b/nix/host-modules/hive-c0re/options.nix @@ -7,17 +7,10 @@ { pkgs, lib, - config, ... }: { options.services.hyperhive.c0re = { - enable = lib.mkOption { - type = lib.types.bool; - default = config.services.hyperhive.enable; - defaultText = lib.literalExpression "config.services.hyperhive.enable"; - description = "Enable hive-c0re coordinator daemon (auto-enabled by services.hyperhive.enable)."; - }; package = lib.mkOption { type = lib.types.package; defaultText = lib.literalExpression "hyperhive.packages.\${system}.default"; diff --git a/nix/host-modules/hive-forge/default.nix b/nix/host-modules/hive-forge/default.nix index dea7363e..aef41468 100644 --- a/nix/host-modules/hive-forge/default.nix +++ b/nix/host-modules/hive-forge/default.nix @@ -459,7 +459,7 @@ in }; }; - config = lib.mkIf (config.services.hyperhive.enable && deployCfg.forgejo.enable) { + config = lib.mkIf deployCfg.forgejo.enable { # Same principle as the vhost below — this service's own surface lives # with the service. The SSO source is named alongside forgejo because # its failure mode is a login that silently falls back, not an error. diff --git a/nix/host-modules/hive-matrix.nix b/nix/host-modules/hive-matrix.nix index 276d8728..4ef3971d 100644 --- a/nix/host-modules/hive-matrix.nix +++ b/nix/host-modules/hive-matrix.nix @@ -86,7 +86,8 @@ let # saying so, as the token path below. # # `hiveName` has no fallback here for the reason ./glue-matrix-bao-token.nix - # gives at its own use of it: it is asserted set for every hyperhive host. + # gives at its own use of it: it is asserted set on every host that runs the + # homeserver. hiveLocalpart = "hive-${toString config.services.hyperhive.hiveName}"; # The `as_token`, and the `hs_token` the spec requires alongside it. Both diff --git a/nix/host-modules/hive-network.nix b/nix/host-modules/hive-network.nix index 9c1c608e..a50b12d2 100644 --- a/nix/host-modules/hive-network.nix +++ b/nix/host-modules/hive-network.nix @@ -35,7 +35,7 @@ in imports = [ (lib.mkRemovedOptionModule [ "services" "hyperhive" "network" "isolateContainers" ] '' Network isolation is the only mode and is always on whenever - hyperhive is enabled; the shared-netns path was removed. Remove + the hive runs; the shared-netns path was removed. Remove the setting. '') (lib.mkRemovedOptionModule [ "services" "hyperhive" "network" "upstreamDns" ] '' @@ -148,11 +148,12 @@ in }; config = lib.mkMerge [ - # Identity checks belong to hyperhive being on, not to the bridge + # Identity checks belong to the hive being on, not to the bridge # being up: a hive with no domain is misconfigured either way, and # moving them under the bridge gate would hide them on exactly the - # hosts that are hardest to debug. - (lib.mkIf config.services.hyperhive.enable { + # hosts that are hardest to debug. `hiveName` is asserted in + # ./hyperhive.nix, where more than the hive depends on it. + (lib.mkIf config.services.hyperhive.deploy.hive-controller.enable { # This message is only useful if an operator can actually reach # it, and an assertion competes with every eager default that # reads the value it guards: option defaults that interpolate the @@ -196,16 +197,6 @@ in `services.hyperhive.domain` for you. ''; } - { - assertion = config.services.hyperhive.hiveName != null; - message = '' - hyperhive requires services.hyperhive.hiveName to be set — - it is this hive's label within the swarm, and the leftmost - part of the domain it is addressed by - (`.`), not only a display name. - Set it (`services.hyperhive.hiveName = "pr1ma";`). - ''; - } ]; }) diff --git a/nix/host-modules/hive-priv.nix b/nix/host-modules/hive-priv.nix index 05fabadb..48c70b7d 100644 --- a/nix/host-modules/hive-priv.nix +++ b/nix/host-modules/hive-priv.nix @@ -19,7 +19,7 @@ let ''; in { - config = lib.mkIf cfg.enable { + config = lib.mkIf config.services.hyperhive.deploy.hive-controller.enable { # Socket unit for hive-priv — the narrow root helper that executes # privileged operations on behalf of hive-c0re. Systemd creates and # holds `/run/hive/priv.sock` before the first connection arrives. diff --git a/nix/host-modules/hive-tls.nix b/nix/host-modules/hive-tls.nix index bba58402..3a9671ec 100644 --- a/nix/host-modules/hive-tls.nix +++ b/nix/host-modules/hive-tls.nix @@ -37,12 +37,11 @@ let # The host-managed hive CA is the trust anchor for self-signed mode. # It is only stood up when the gateway actually serves a self-signed - # cert: the gateway must be in self-signed mode. `domain` is required - # (asserted in hive-network.nix), so the leaf SANs always have a - # domain to derive from. The self-signed condition is the gateway - # module's single source of truth (`gateway.useSelfSigned`): true when - # neither an operator cert (`tls.certDir`) nor ACME is set. - active = hyperhiveCfg.enable && gatewayCfg.useSelfSigned; + # cert: the gateway must run here and be in self-signed mode. The + # self-signed condition is the gateway module's single source of truth + # (`gateway.useSelfSigned`): true when neither an operator cert + # (`tls.certDir`) nor ACME is set. + active = gatewayCfg.enable && gatewayCfg.useSelfSigned; # How this hive's CA comes into existence when it is missing — and it # is one of exactly two things, chosen by config rather than by what @@ -1022,7 +1021,14 @@ in # bundle. It is the ANCHOR bundle rather than `ca.pem` for the reason # spelled out where the bundle is written above; no key path is ever # exposed (an agent that could read one could mint trusted certs). - systemd.services.hive-c0re.environment.HIVE_TLS_CA_PATH = "${cfg.stateDir}/trust-bundle.pem"; + # + # ⚠️ Every line here that names another daemon's unit is gated on that + # daemon's own enable: the gateway also runs on hosts with no hive and no + # controller, and defining an environment key on a unit that does not + # exist CREATES a unit fragment for it — inert (no `ExecStart`, empty + # `wantedBy`, never activated), and it evaluates and builds clean. + systemd.services.hive-c0re.environment.HIVE_TLS_CA_PATH = + lib.mkIf hyperhiveCfg.deploy.hive-controller.enable "${cfg.stateDir}/trust-bundle.pem"; # The same anchor, named for the swarm-queue clients that need it when # they mint a token from authelia over TLS. Declared HERE, beside the @@ -1038,18 +1044,11 @@ in # variable name. The anchor was never missing; nothing pointed the queue # client at it. # - # Set unconditionally within this module's `active` guard, exactly like - # the line above: where there is no hive CA this module contributes - # nothing at all, and the clients then fall back to the platform roots — - # which is correct for a swarm fronted by a public certificate. - systemd.services.hive-c0re.environment.HIVE_C0RE_OIDC_CA_FILE = "${cfg.stateDir}/trust-bundle.pem"; - # ⚠️ Gated, where the hive-c0re line above is not, and the asymmetry is - # the point: hive-c0re runs on every hive, the controller runs on one. - # Defining an environment key on a unit that does not exist CREATES a - # unit fragment for it — inert (no `ExecStart`, empty `wantedBy`, never - # activated) but present on every non-controller hive with a CA. Caught - # in review on this PR; it evaluates and builds clean either way, which - # is exactly why it needed a reviewer rather than a check. + # Where there is no hive CA this module contributes nothing at all, and + # the clients then fall back to the platform roots — which is correct for + # a swarm fronted by a public certificate. + systemd.services.hive-c0re.environment.HIVE_C0RE_OIDC_CA_FILE = + lib.mkIf hyperhiveCfg.deploy.hive-controller.enable "${cfg.stateDir}/trust-bundle.pem"; systemd.services.swarm-controller.environment.SWARM_CONTROLLER_OIDC_CA_FILE = lib.mkIf hyperhiveCfg.deploy.swarm-controller.enable "${cfg.stateDir}/trust-bundle.pem"; }; diff --git a/nix/host-modules/hyperhive.nix b/nix/host-modules/hyperhive.nix index 2925c214..83f7c200 100644 --- a/nix/host-modules/hyperhive.nix +++ b/nix/host-modules/hyperhive.nix @@ -1,7 +1,6 @@ -# Top-level, cross-cutting hyperhive options: the master enable -# switch, the hive's identity (domain + display names), and hive-wide -# feature toggles read by several subsystem modules. Imported by the -# ./default.nix aggregator. +# Top-level, cross-cutting hyperhive options: the hive's identity +# (domain + display names) and hive-wide feature toggles read by several +# subsystem modules. Imported by the ./default.nix aggregator. { lib, config, @@ -22,13 +21,9 @@ in ) ]; - # Top-level hyperhive enable flag. When true, automatically enables - # hive-c0re and the on-by-default hyperhive subsystems. - options.services.hyperhive.enable = lib.mkEnableOption "hyperhive — the agent swarm coordinator"; - # Canonical hive DNS domain shared by every subsystem that needs a # stable hostname. Typed nullOr (default null) so the option always - # exists, but it's REQUIRED whenever hyperhive is enabled — an + # exists, but it's REQUIRED wherever this host runs a hive — an # assertion in hive-network.nix fails eval when it's unset, since # matrix bakes it in on first boot and the gateway/forge/agent URLs all # derive from it (no safe default). Full identity-surface @@ -61,7 +56,8 @@ in stable name (currently: `services.hyperhive.swarm.matrix.serverName` derives from this, defaulting to `matrix.''${services.hyperhive.domain}` when `serverName` is - null). **Required** when `services.hyperhive.enable` — eval fails + null). **Required** when + `services.hyperhive.deploy.hive-controller.enable` — eval fails with a helpful message if it's unset (it's baked into matrix on first boot and drives the gateway/forge/agent URLs, with no safe default; changing it later is destructive). Exposed to agents as @@ -88,24 +84,22 @@ in # nothing — measured, after this warning fired on the conventional # case. `mkOptionDefault` is priority 1500, so anything lower is a # definition someone actually wrote (100 plain, 1000 mkDefault). - config.warnings = - lib.optional (hiveCfg.enable && options.services.hyperhive.domain.highestPrio < 1500) - '' - services.hyperhive.domain is set explicitly and is deprecated. This - hive's address belongs in the swarm directory, which every hive in - the swarm shares a copy of: + config.warnings = lib.optional (options.services.hyperhive.domain.highestPrio < 1500) '' + services.hyperhive.domain is set explicitly and is deprecated. This + hive's address belongs in the swarm directory, which every hive in + the swarm shares a copy of: - services.hyperhive.swarm.hives."${toString hiveCfg.hiveName}".domain = "${toString hiveCfg.domain}"; + services.hyperhive.swarm.hives."${toString hiveCfg.hiveName}".domain = "${toString hiveCfg.domain}"; - (Or drop the value entirely if it is the conventional - `.` — that is the directory entry's own - default.) + (Or drop the value entirely if it is the conventional + `.` — that is the directory entry's own + default.) - Setting it here still wins, so nothing is broken right now. What it - does not do is tell the other hives: they read this hive's address - out of their copy of the directory, so a value written only here - leaves them pointing somewhere else with nothing detecting it. - ''; + Setting it here still wins, so nothing is broken right now. What it + does not do is tell the other hives: they read this hive's address + out of their copy of the directory, so a value written only here + leaves them pointing somewhere else with nothing detecting it. + ''; # Where the swarm lives. Declared beside the hive's own identity # because it is what that identity is derived FROM — every hive in a @@ -123,7 +117,8 @@ in `services.hyperhive.hiveName`, list the hives by name, and no hive in the swarm states an address at all. - **Required** when `services.hyperhive.enable`, and deliberately + **Required** when + `services.hyperhive.deploy.hive-controller.enable`, and deliberately not defaulted: there is no fallback worth having. A guessed swarm domain is a wrong hostname that evaluates cleanly and deploys, which is worse than an eval failure telling an @@ -144,7 +139,10 @@ in example = "pr1ma"; description = '' Human-readable name of this single-host hive instance. - **Required** when `services.hyperhive.enable`. Distinct from + **Required** when this host runs a hive, the swarm's secret store + or its homeserver + (`services.hyperhive.deploy.hive-controller.enable`, + `deploy.bao.enable`, `deploy.matrix.enable`). Distinct from `services.hyperhive.domain` (the machine-addressable DNS name) but no longer merely cosmetic: a hive occupies `.`, so this is the label the hive is @@ -154,6 +152,28 @@ in ''; }; + # Each of these deployments turns `hiveName` into an identifier with no + # fallback: hive-c0re's OIDC client and matrix localpart, the CN of the + # store's `client.pem` (./glue-bao-tls.nix), and the `hives/` path + # the matrix token and queue credential readers fetch. A null there renders + # as an empty string that evaluates and deploys. + config.assertions = [ + { + assertion = + !( + hiveCfg.deploy.hive-controller.enable || hiveCfg.deploy.bao.enable || hiveCfg.deploy.matrix.enable + ) + || hiveCfg.hiveName != null; + message = '' + hyperhive requires services.hyperhive.hiveName to be set on a host + that runs a hive, the swarm's secret store or its homeserver — it is + this hive's label within the swarm, and the leftmost part of the + domain it is addressed by (`.`), not only a + display name. Set it (`services.hyperhive.hiveName = "pr1ma";`). + ''; + } + ]; + # The one hive-level option that describes something ABOVE the hive, # which is why it sits under `swarm` with the swarm-global services # rather than beside `hiveName`. diff --git a/nix/host-modules/swarm-authelia.nix b/nix/host-modules/swarm-authelia.nix index dcdafdc3..1da10dee 100644 --- a/nix/host-modules/swarm-authelia.nix +++ b/nix/host-modules/swarm-authelia.nix @@ -919,7 +919,7 @@ in }; }; - config = lib.mkIf (hyperhiveCfg.enable && deployCfg.authelia.enable) { + config = lib.mkIf deployCfg.authelia.enable { # The derived half of the client list, declared the same way an # operator declares one. Everything downstream then reads a single # uniformly-typed `cfg.oidc.clients` and cannot tell the parts apart — diff --git a/nix/host-modules/swarm-bao.nix b/nix/host-modules/swarm-bao.nix index 44404842..c517e763 100644 --- a/nix/host-modules/swarm-bao.nix +++ b/nix/host-modules/swarm-bao.nix @@ -1573,7 +1573,7 @@ in # they still fire when the cert paths are unset — the arm below is not # evaluated in that case, and an assertion that disappears exactly when # its subject is broken would be worse than none. - (lib.mkIf (hyperhiveCfg.enable && deployCfg.bao.enable) { + (lib.mkIf deployCfg.bao.enable { assertions = [ { assertion = lib.versionOlder baoDeploy.package.version "2.7.0"; @@ -1655,7 +1655,7 @@ in services.hyperhive.network.enable = lib.mkDefault true; }) - (lib.mkIf (hyperhiveCfg.enable && deployCfg.bao.enable) { + (lib.mkIf deployCfg.bao.enable { # The wrapper alone, never `baoDeploy.package`: openbao's own module # installs the CLI *inside* the container, and this host had none at all, # so an operator either hopped into the container or re-typed the @@ -1845,7 +1845,7 @@ in }; }) - (lib.mkIf (hyperhiveCfg.enable && deployCfg.bao.enable && haveServerTls) { + (lib.mkIf (deployCfg.bao.enable && haveServerTls) { # Provisions the TPM-backed token the seal above names. One-shot and # idempotent on ABSENCE, never on content: regenerating a PIN would # orphan an already-sealed store, so a rebuild must not rotate it. diff --git a/nix/host-modules/swarm-ca.nix b/nix/host-modules/swarm-ca.nix index a434b655..facb01b1 100644 --- a/nix/host-modules/swarm-ca.nix +++ b/nix/host-modules/swarm-ca.nix @@ -106,7 +106,7 @@ in }; }; - config = lib.mkIf (hyperhiveCfg.enable && cfg.autoConfigure) { + config = lib.mkIf cfg.autoConfigure { # A CA that fails to issue is invisible until something makes a TLS call # hours later, so this oneshot is worth more than most services. services.hyperhive.swarm.otel.journaldUnits = [ "swarm-ca" ]; diff --git a/nix/host-modules/swarm-controller.nix b/nix/host-modules/swarm-controller.nix index 9dc6325e..336a017c 100644 --- a/nix/host-modules/swarm-controller.nix +++ b/nix/host-modules/swarm-controller.nix @@ -349,7 +349,7 @@ in # `enable` moved to `services.hyperhive.deploy.swarm-controller.enable` — see # ./deploy.nix. `services.hyperhive.deploy.singleHostSwarm` still # asserts it, and that was never an exception to "not derived from - # services.hyperhive.enable": that mode says "this box is the whole + # deploy.hive-controller.enable": that mode says "this box is the whole # deployment", which answers the topology question outright, where # `enable` alone never can. Both packages moved to that same # `deploy.swarm-controller` block. What stays here is what the daemon IS. diff --git a/nix/host-modules/swarm-grafana.nix b/nix/host-modules/swarm-grafana.nix index 85e5a2d1..b447427a 100644 --- a/nix/host-modules/swarm-grafana.nix +++ b/nix/host-modules/swarm-grafana.nix @@ -405,7 +405,7 @@ in }; }; - config = lib.mkIf (hyperhiveCfg.enable && deployCfg.grafana.enable) { + config = lib.mkIf deployCfg.grafana.enable { # The gateway name and the quick-link, both inside `deploy.grafana` — that # guard is the load-bearing part. Every hive in a swarm may know this UI # exists, but only the host that RUNS it may claim the name; a client diff --git a/nix/host-modules/swarm-secret-publisher.nix b/nix/host-modules/swarm-secret-publisher.nix index cfe8f5d1..36e1f732 100644 --- a/nix/host-modules/swarm-secret-publisher.nix +++ b/nix/host-modules/swarm-secret-publisher.nix @@ -47,7 +47,7 @@ let # Both halves have to be here: the mint (authelia, for the plaintext) and an # identity (for the store). Neither implies the other. - active = hyperhiveCfg.enable && cfg.enable && deployCfg.authelia.enable && haveClientIdentity; + active = cfg.enable && deployCfg.authelia.enable && haveClientIdentity; hiveNames = lib.attrNames hyperhiveCfg.swarm.hives; diff --git a/nix/host-modules/swarm-victorialogs.nix b/nix/host-modules/swarm-victorialogs.nix index d312b5cf..677010b6 100644 --- a/nix/host-modules/swarm-victorialogs.nix +++ b/nix/host-modules/swarm-victorialogs.nix @@ -134,7 +134,7 @@ in ''; }; - config = lib.mkIf (hyperhiveCfg.enable && deployCfg.victorialogs.enable) { + config = lib.mkIf deployCfg.victorialogs.enable { # This store publishes its own health as prometheus metrics on the same # listener it serves queries on, so the swarm's collector scrapes it with # no exporter and no extra port — same arrangement as the metrics store. diff --git a/nix/host-modules/swarm-victoriametrics.nix b/nix/host-modules/swarm-victoriametrics.nix index 90430733..14ce09ab 100644 --- a/nix/host-modules/swarm-victoriametrics.nix +++ b/nix/host-modules/swarm-victoriametrics.nix @@ -95,7 +95,7 @@ in ''; }; - config = lib.mkIf (hyperhiveCfg.enable && deployCfg.victoriametrics.enable) { + config = lib.mkIf deployCfg.victoriametrics.enable { # The gateway name and the quick-link, both inside `deployCfg.victoriametrics.enable` — that # guard is the load-bearing part. Every hive in a swarm may know this # store exists, but only the host that RUNS it may claim the name; a diff --git a/nix/host-modules/swarm-wireguard.nix b/nix/host-modules/swarm-wireguard.nix index 7b2e1694..fb913f8e 100644 --- a/nix/host-modules/swarm-wireguard.nix +++ b/nix/host-modules/swarm-wireguard.nix @@ -84,13 +84,11 @@ }; }; - # Gated on the mesh itself, NOT on the c0re daemon. The mesh is host - # networking, not a c0re feature: a swarm host that runs no hive — - # the snapshot store, for one — still has to join the mesh, and under - # the old `c0re.enable` gate it silently got no `wg-hive` interface - # at all. Nothing below is c0re-specific; the peer data c0re consumes - # (HIVE_PEER_CA_PATHS) is rendered in ./hive-c0re and stays gated - # there. + # Gated on the mesh itself, NOT on `deploy.hive-controller.enable`. The + # mesh is host networking, not a c0re feature: a swarm host that runs no + # hive — the snapshot store, for one — still has to join the mesh. + # Nothing below is c0re-specific; the peer data c0re consumes + # (HIVE_PEER_CA_PATHS) is rendered in ./hive-c0re and stays gated there. config = lib.mkIf config.services.hyperhive.deploy.wireguard.enable { assertions = [ { diff --git a/nix/module-eval/bao-matrix-reader.nix b/nix/module-eval/bao-matrix-reader.nix index 45ab3e46..9f599c56 100644 --- a/nix/module-eval/bao-matrix-reader.nix +++ b/nix/module-eval/bao-matrix-reader.nix @@ -70,7 +70,8 @@ let # The same omission on a hive that does NOT run a homeserver. Separates the # matrix refusal's `deploy.matrix.enable` clause from the queue refusal, which - # has no toggle to check — an arm below reads exactly one refusal off it. + # checks only the hive's own toggle — an arm below reads exactly one refusal + # off it. remoteReaderNoMatrixMissingLeaves = hive { deploy.bao.clientCertFile = "/etc/pki/bao-client.pem"; deploy.bao.clientKeyFile = "/etc/pki/bao-client-key.pem"; diff --git a/nix/module-eval/core-toggle.nix b/nix/module-eval/core-toggle.nix index 76fae596..53aecc1e 100644 --- a/nix/module-eval/core-toggle.nix +++ b/nix/module-eval/core-toggle.nix @@ -30,17 +30,17 @@ let bare = hive { }; - # The same stub with the central toggle off. Paired with `bare` below to pin - # the defaults that used to read `services.hyperhive.enable` and no longer - # do: each is asserted to hold the SAME literal in both, so a future edit - # that quietly re-introduces the dependency — or that changes what the + # The same stub with the central toggle (`deploy.hive-controller.enable`) + # off. Paired with `bare` below to pin the defaults that do not read it: + # each is asserted to hold the SAME literal in both, so a future edit + # that quietly introduces the dependency — or that changes what the # default renders for a hive with the toggle on — fails here. Reading an # option off this fixture forces that option only, not the config, so the # toggle being off costs nothing. Also the "installs the modules and turns # nothing on" host the swarm-service absences below read: none of the # per-service deployment toggles derives from the hive being on, so it # renders the same absences `bare` does. - centralToggleOff = hive { enable = false; }; + centralToggleOff = hive { deploy.hive-controller.enable = false; }; # Every hive defaults the agents' queue address to the queue's name, so the # only hive without one is a hive told to have none. @@ -104,7 +104,7 @@ let name = "the swarm controller's forgeTokenFile default does not consult the central toggle"; ok = (hive { - enable = false; + deploy.hive-controller.enable = false; deploy.forgejo.enable = true; }).services.hyperhive.deploy.swarm-controller.forgeTokenFile == "/var/lib/hyperhive-forge/swarm-controller.token"; @@ -453,6 +453,55 @@ let && bare.services.dnsmasq.enable && bare.networking.bridges ? hive-br0; } + { + # A host config written against either old spelling still runs its hive. + # The builder's own `true` is lowered to `mkDefault false`, so only the + # forwarded definition can turn it on. + name = "the old hive toggles forward to deploy.hive-controller.enable and warn"; + ok = + let + renamedFrom = + old: m: + lib.any ( + w: lib.hasInfix old w && lib.hasInfix "services.hyperhive.deploy.hive-controller.enable" w + ) m.warnings; + viaOld = + extra: + hive ( + extra + // { + deploy.hive-controller.enable = lib.mkDefault false; + } + ); + oldEnable = viaOld { enable = true; }; + oldC0re = viaOld { c0re.enable = true; }; + in + oldEnable.services.hyperhive.deploy.hive-controller.enable + && oldEnable.systemd.services ? hive-c0re + && renamedFrom "services.hyperhive.enable" oldEnable + && oldC0re.services.hyperhive.deploy.hive-controller.enable + && oldC0re.systemd.services ? hive-c0re + && renamedFrom "services.hyperhive.c0re.enable" oldC0re; + } + { + # Refused wherever something turns the name into an identifier, and only + # there. The store host is the control that the refusal is not the hive's + # alone; the host running neither is the control that it is gated at all. + name = "a missing hiveName is refused on a hive and on a store host, not elsewhere"; + ok = + let + refusedHiveName = + extra: + lib.any (a: !a.assertion && lib.hasInfix "services.hyperhive.hiveName to be set" a.message) + (hive ({ hiveName = null; } // extra)).assertions; + in + refusedHiveName { } + && refusedHiveName { + deploy.hive-controller.enable = false; + deploy.bao.enable = true; + } + && !(refusedHiveName { deploy.hive-controller.enable = false; }); + } ]; in runGroup "core-toggle" cases diff --git a/nix/module-eval/lib.nix b/nix/module-eval/lib.nix index cc25daf6..1b219351 100644 --- a/nix/module-eval/lib.nix +++ b/nix/module-eval/lib.nix @@ -59,7 +59,7 @@ let # just one level down. `recursiveUpdate` merges nested attrsets # all the way down instead. services.hyperhive = lib.recursiveUpdate { - enable = true; + deploy.hive-controller.enable = true; hiveName = "h1"; swarm.domain = "t.local"; swarm.hives.h1.domain = "h1.t.local"; diff --git a/nix/module-eval/swarm-services-switch.nix b/nix/module-eval/swarm-services-switch.nix index 107123c5..af22eaca 100644 --- a/nix/module-eval/swarm-services-switch.nix +++ b/nix/module-eval/swarm-services-switch.nix @@ -23,20 +23,37 @@ let swarmServiceEnables ; - # The swarm-services toggle with the central one off, so the only thing - # that can enable the gateway/resolver/bridge here is that toggle's own - # module — every other module that asserts them is behind `enable`. + # A dedicated services host with hives elsewhere: the swarm-services + # toggle and no hive, the recipe docs/swarm/services.md gives. swarmServicesOnly = hive { - enable = false; + 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 { - enable = false; + deploy.hive-controller.enable = false; network.exposeHostPorts = [ 5432 ]; }; @@ -129,9 +146,44 @@ let # module rather than from any of their defaults. name = "the swarm-services toggle turns on the gateway, resolver and bridge by itself"; ok = - swarmServicesOnly.services.hyperhive.gateway.enable - && swarmServicesOnly.services.hyperhive.gateway.dns.enable - && swarmServicesOnly.services.hyperhive.network.enable; + 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, diff --git a/swarm-controller/README.md b/swarm-controller/README.md index ace621ea..05a3d19f 100644 --- a/swarm-controller/README.md +++ b/swarm-controller/README.md @@ -5,8 +5,9 @@ owns what is true _across_ hives — so a swarm runs one of them and most hives leave it off. Opt-in per host via `services.hyperhive.deploy.swarm-controller.enable`, which is -deliberately **not** derived from `services.hyperhive.enable`: turning it on is -a statement about swarm topology, not about whether hyperhive is installed. +deliberately **not** derived from +`services.hyperhive.deploy.hive-controller.enable`: turning it on is a statement +about swarm topology, not about whether this host runs a hive. ## What it does today