Watch
0
0
Fork
You've already forked hyperhive
0

Compare commits

..
43 changed files with 168 additions and 299 deletions

View file

@ -77,7 +77,7 @@ Minimal `flake.nix` for a host that runs hive-c0re:
modules = [ modules = [
hyperhive.nixosModules.default # hive-c0re + hive-forge + hive-gateway in one import hyperhive.nixosModules.default # hive-c0re + hive-forge + hive-gateway in one import
({ ... }: { ({ ... }: {
services.hyperhive.deploy.hive-controller.enable = true; services.hyperhive.enable = true;
# services.hyperhive.c0re.operatorPronouns = "they/them"; # default: "she/her" # services.hyperhive.c0re.operatorPronouns = "they/them"; # default: "she/her"
# ... rest of your host config # ... rest of your host config

View file

@ -90,7 +90,7 @@ listener on `bridgeIp` is on the host's bridge interface.
```nix ```nix
{ {
services.hyperhive = { services.hyperhive = {
deploy.hive-controller.enable = true; enable = true;
hiveName = "pr1ma"; hiveName = "pr1ma";
swarm.domain = "darkest.space"; swarm.domain = "darkest.space";
swarm.hives.pr1ma = { }; # -> domain = pr1ma.darkest.space swarm.hives.pr1ma = { }; # -> domain = pr1ma.darkest.space

View file

@ -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. [swarm/](../swarm/README.md) for the mesh itself.
Note that `deploy.wireguard.enable` gates the mesh, **not** Note that `deploy.wireguard.enable` gates the mesh, **not**
`deploy.hive-controller.enable` --- a store host runs no hive and would otherwise get no `c0re.enable` --- a store host runs no hive and would otherwise get no
`wg-hive` interface at all. `wg-hive` interface at all.
## Pointing a hive at it ## Pointing a hive at it

View file

@ -299,9 +299,9 @@ migrating agent keeps one unbroken incremental chain. See
`services.hyperhive.deploy.swarm-controller.enable` runs the `swarm-controller` `services.hyperhive.deploy.swarm-controller.enable` runs the `swarm-controller`
daemon on this host. **Off by default and deliberately not derived from daemon on this host. **Off by default and deliberately not derived from
`services.hyperhive.deploy.hive-controller.enable`**: a swarm has one `services.hyperhive.enable`**: a swarm has one controller, so enabling it
controller, so enabling it states a fact about swarm topology, not about is a statement about swarm topology, not about whether the operator has
whether this host runs a hive. Every hive runs `hive-c0re` (the agents on that host); one installed hyperhive. Every hive runs `hive-c0re` (the agents on that host); one
hive additionally runs this (what's true across hives). hive additionally runs this (what's true across hives).
What it serves, why it's a unix socket rather than a port, and the What it serves, why it's a unix socket rather than a port, and the

View file

@ -119,7 +119,7 @@
# an operator override still wins. Intended usage: # an operator override still wins. Intended usage:
# #
# imports = [ hyperhive.nixosModules.default ]; # imports = [ hyperhive.nixosModules.default ];
# services.hyperhive.deploy.hive-controller.enable = true; # services.hyperhive.enable = true;
# #
default = default =
{ lib, pkgs, ... }: { lib, pkgs, ... }:

View file

@ -54,7 +54,7 @@ let
}; };
boot.loader.grub.enable = false; boot.loader.grub.enable = false;
system.stateVersion = "25.11"; system.stateVersion = "25.11";
services.hyperhive.deploy.hive-controller.enable = lib.mkForce false; services.hyperhive.enable = lib.mkForce false;
services.hyperhive.deploy.matrix.enable = lib.mkForce false; services.hyperhive.deploy.matrix.enable = lib.mkForce false;
} }
) )

View file

@ -1,8 +1,7 @@
# The full hyperhive host stack, pulled together in one place — this # The full hyperhive host stack, pulled together in one place — this
# is what the flake exports as `nixosModules.default` (wrapped with # is what the flake exports as `nixosModules.default` (wrapped with
# the package/source wiring; see flake.nix). One import covers # the package/source wiring; see flake.nix). One import covers
# everything; `services.hyperhive.deploy.hive-controller.enable = true` # everything; `services.hyperhive.enable = true` turns the stack on.
# runs a hive on this host.
# #
# Every swarm needs the forge — hive-c0re mirrors every agent's applied # 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 # config repo into it and it's the canonical store for the meta flake
@ -10,7 +9,7 @@
# runs it (derived from `deploy.allSwarmServices`, see # runs it (derived from `deploy.allSwarmServices`, see
# ./swarm-required-services.nix). hive-matrix is opt-in (off by default). All # ./swarm-required-services.nix). hive-matrix is opt-in (off by default). All
# subsystems rely on `services.hyperhive.domain`, which is required # subsystems rely on `services.hyperhive.domain`, which is required
# (asserted in hive-network.nix) wherever this host runs a hive. # (asserted in hive-network.nix) whenever hyperhive is enabled.
{ {
imports = [ imports = [
./hyperhive.nix ./hyperhive.nix

View file

@ -119,15 +119,6 @@ in
[ "services" "hyperhive" "swarm" "statusPublish" "clientSecretFile" ] [ "services" "hyperhive" "swarm" "statusPublish" "clientSecretFile" ]
[ "services" "hyperhive" "deploy" "hive-controller" "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 (lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "otel" "enable" ] [ "services" "hyperhive" "swarm" "otel" "enable" ]
[ "services" "hyperhive" "deploy" "swarm-otel" "enable" ] [ "services" "hyperhive" "deploy" "swarm-otel" "enable" ]
@ -461,9 +452,9 @@ in
Run the swarm's metrics UI on this host. Run the swarm's metrics UI on this host.
Off by default and not derived from Off by default and not derived from
{option}`services.hyperhive.deploy.hive-controller.enable`: a swarm {option}`services.hyperhive.enable`: a swarm has one Grafana, so
has one Grafana, so running it is a decision about this host rather running it is a decision about this host rather than about
than about whether it runs a hive. whether hyperhive is installed.
''; '';
}; };
@ -579,21 +570,6 @@ 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 { swarm-controller.enable = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
@ -601,9 +577,9 @@ in
Run the swarm-controller daemon on this host. Run the swarm-controller daemon on this host.
Off by default and deliberately not derived from Off by default and deliberately not derived from
{option}`services.hyperhive.deploy.hive-controller.enable`: a swarm {option}`services.hyperhive.enable`: a swarm has one controller,
has one controller, so running it is a decision about this host so running it is a decision about this host rather than about
rather than about whether it runs a hive. whether hyperhive is installed.
''; '';
}; };

View file

@ -31,21 +31,22 @@ let
readersHere = { readersHere = {
# ./glue-matrix-bao-token.nix # ./glue-matrix-bao-token.nix
swarm-bao-matrix-token = swarm-bao-matrix-token =
havePair baoDeploy.matrixTokenClientCertFile baoDeploy.matrixTokenClientKeyFile hyperhiveCfg.enable
&& havePair baoDeploy.matrixTokenClientCertFile baoDeploy.matrixTokenClientKeyFile
&& deployCfg.matrix.enable; && deployCfg.matrix.enable;
# ./glue-queue-agent-credential.nix # ./glue-queue-agent-credential.nix
swarm-bao-queue-agent = swarm-bao-queue-agent =
deployCfg.hive-controller.enable hyperhiveCfg.enable
&& havePair baoDeploy.queueAgentClientCertFile baoDeploy.queueAgentClientKeyFile; && havePair baoDeploy.queueAgentClientCertFile baoDeploy.queueAgentClientKeyFile;
# ./swarm-grafana.nix # ./swarm-grafana.nix
swarm-bao-grafana-oidc = deployCfg.grafana.enable; swarm-bao-grafana-oidc = hyperhiveCfg.enable && deployCfg.grafana.enable;
# ./swarm-otel.nix # ./swarm-otel.nix
swarm-bao-otel-oidc = swarm-bao-otel-oidc =
deployCfg.swarm-otel.enable deployCfg.swarm-otel.enable
&& havePair baoDeploy.otelOidcClientCertFile baoDeploy.otelOidcClientKeyFile; && havePair baoDeploy.otelOidcClientCertFile baoDeploy.otelOidcClientKeyFile;
# ./swarm-bao.nix: its block is gated on the store, which `orderAfterPolicy` # ./swarm-bao.nix: its block is gated on the store, which `orderAfterPolicy`
# already checks. # already checks.
swarm-bao-forwarder-oidc = true; swarm-bao-forwarder-oidc = hyperhiveCfg.enable;
# ./swarm-nats.nix: the queue's TLS leaf, not a secret, but the same wait. # ./swarm-nats.nix: the queue's TLS leaf, not a secret, but the same wait.
swarm-bao-nats-tls = deployCfg.nats.enable; swarm-bao-nats-tls = deployCfg.nats.enable;
}; };

View file

@ -43,10 +43,9 @@ let
# What a reader calls itself to the store. The hive's name, because a bao # 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. # cert-auth role matches on the CN — this is an interface, not a label.
# No fallback: `hiveName` is asserted set wherever `deploy.bao.enable` is # No fallback: `hiveName` is asserted set for every hyperhive host, which is
# (./hyperhive.nix), the same condition this file's `config` is gated on. A # the same condition this file's `config` is gated on. A fallback here reads
# fallback here reads as a second supported spelling and there is no such # as a second supported spelling and there is no such thing.
# thing.
clientCn = hyperhiveCfg.hiveName; clientCn = hyperhiveCfg.hiveName;
# $1 dir $2 basename $3 CN $4 SAN or "" $5 EKU # $1 dir $2 basename $3 CN $4 SAN or "" $5 EKU
@ -72,7 +71,7 @@ let
''; '';
in in
{ {
config = lib.mkIf deployCfg.bao.enable { config = lib.mkIf (hyperhiveCfg.enable && deployCfg.bao.enable) {
services.hyperhive.deploy.bao = { services.hyperhive.deploy.bao = {
serverCertFile = lib.mkDefault "${pkiDir}/server.pem"; serverCertFile = lib.mkDefault "${pkiDir}/server.pem";
serverKeyFile = lib.mkDefault "${pkiDir}/server-key.pem"; serverKeyFile = lib.mkDefault "${pkiDir}/server-key.pem";

View file

@ -32,7 +32,7 @@ let
pkiDir = if haveMintedPki then builtins.dirOf baoDeploy.clientCertFile else null; pkiDir = if haveMintedPki then builtins.dirOf baoDeploy.clientCertFile else null;
in in
{ {
config = lib.mkIf (deployCfg.swarm-controller.enable && haveMintedPki) { config = lib.mkIf (hyperhiveCfg.enable && deployCfg.swarm-controller.enable && haveMintedPki) {
services.hyperhive.deploy.swarm-controller = { services.hyperhive.deploy.swarm-controller = {
baoClientCertFile = lib.mkDefault "${pkiDir}/controller.pem"; baoClientCertFile = lib.mkDefault "${pkiDir}/controller.pem";
baoClientKeyFile = lib.mkDefault "${pkiDir}/controller-key.pem"; baoClientKeyFile = lib.mkDefault "${pkiDir}/controller-key.pem";

View file

@ -24,7 +24,7 @@ let
forgeCfg = hyperhiveCfg.swarm.forge; forgeCfg = hyperhiveCfg.swarm.forge;
in in
{ {
config = lib.mkIf deployCfg.authelia.enable { config = lib.mkIf (hyperhiveCfg.enable && deployCfg.authelia.enable) {
# One declaration, two readers. The forge knows its own callback URL; # One declaration, two readers. The forge knows its own callback URL;
# making the operator restate it in authelia's client list would be a # 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 # second source of truth for a string whose mismatch is a silent

View file

@ -32,7 +32,7 @@ let
grafanaCfg = hyperhiveCfg.swarm.grafana; grafanaCfg = hyperhiveCfg.swarm.grafana;
in in
{ {
config = lib.mkIf deployCfg.authelia.enable { config = lib.mkIf (hyperhiveCfg.enable && deployCfg.authelia.enable) {
# One declaration, two readers. Grafana's callback URL is format-locked to # 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 # 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 # format is spelled — restating it here would be a second source of truth

View file

@ -69,8 +69,7 @@ let
# pieces; this literal is the nix half of that one agreement. # 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 # `hiveName` has no fallback here for the reason ./glue-bao-tls.nix gives at
# its own use of it: it is asserted set on every host that runs the # its own use of it: it is asserted set for every hyperhive host.
# homeserver.
tokenPath = "secret/swarm/hives/${hyperhiveCfg.hiveName}/matrix/appservice-token"; tokenPath = "secret/swarm/hives/${hyperhiveCfg.hiveName}/matrix/appservice-token";
# A literal, not an option — ./hive-matrix.nix names its container # A literal, not an option — ./hive-matrix.nix names its container
@ -95,7 +94,7 @@ in
# is a hive that has no store, which is supported. So this one additionally # is a hive that has no store, which is supported. So this one additionally
# requires `hiveReaderIdentity`: the host demonstrably reads the store, and # requires `hiveReaderIdentity`: the host demonstrably reads the store, and
# named every option but this pair. # named every option but this pair.
(lib.mkIf (deployCfg.matrix.enable && hiveReaderIdentity) { (lib.mkIf (hyperhiveCfg.enable && deployCfg.matrix.enable && hiveReaderIdentity) {
assertions = [ assertions = [
{ {
assertion = haveClientIdentity; assertion = haveClientIdentity;
@ -125,7 +124,7 @@ in
]; ];
}) })
(lib.mkIf (haveClientIdentity && deployCfg.matrix.enable) { (lib.mkIf (hyperhiveCfg.enable && haveClientIdentity && deployCfg.matrix.enable) {
# Same rule as the unit's own gate: this reader exists on a host that has a # 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 # client identity and a homeserver, which is not every host that runs the
# store, so the store's module cannot name it. # store, so the store's module cannot name it.

View file

@ -31,7 +31,7 @@ let
pkiDir = if haveMintedPki then builtins.dirOf baoDeploy.clientCertFile else null; pkiDir = if haveMintedPki then builtins.dirOf baoDeploy.clientCertFile else null;
in in
{ {
config = lib.mkIf (deployCfg.matrix.enable && haveMintedPki) { config = lib.mkIf (hyperhiveCfg.enable && deployCfg.matrix.enable && haveMintedPki) {
services.hyperhive.deploy.matrix = { services.hyperhive.deploy.matrix = {
ctlBaoClientCertFile = lib.mkDefault "${pkiDir}/matrix-ctl.pem"; ctlBaoClientCertFile = lib.mkDefault "${pkiDir}/matrix-ctl.pem";
ctlBaoClientKeyFile = lib.mkDefault "${pkiDir}/matrix-ctl-key.pem"; ctlBaoClientKeyFile = lib.mkDefault "${pkiDir}/matrix-ctl-key.pem";

View file

@ -31,7 +31,7 @@ let
pkiDir = if haveMintedPki then builtins.dirOf baoDeploy.clientCertFile else null; pkiDir = if haveMintedPki then builtins.dirOf baoDeploy.clientCertFile else null;
in in
{ {
config = lib.mkIf (deployCfg.nats.enable && haveMintedPki) { config = lib.mkIf (hyperhiveCfg.enable && deployCfg.nats.enable && haveMintedPki) {
services.hyperhive.deploy.nats = { services.hyperhive.deploy.nats = {
baoClientCertFile = lib.mkDefault "${pkiDir}/nats.pem"; baoClientCertFile = lib.mkDefault "${pkiDir}/nats.pem";
baoClientKeyFile = lib.mkDefault "${pkiDir}/nats-key.pem"; baoClientKeyFile = lib.mkDefault "${pkiDir}/nats-key.pem";

View file

@ -20,10 +20,10 @@
# into one that is refused, which reaches the agent as a timeout. # into one that is refused, which reaches the agent as a timeout.
# #
# 📌 This runs wherever a client identity is configured, NOT only where the # 📌 This runs wherever a client identity is configured, NOT only where the
# store is — the rule ./glue-matrix-bao-token.nix states in full — and # store is — the rule ./glue-matrix-bao-token.nix states in full. There is no
# wherever this host runs a hive (`deploy.hive-controller.enable`): the # "this hive runs agents" condition to gate it on as well: agent containers
# credential is for the agents hive-c0re creates here at runtime, and the unit # are created at runtime by hive-c0re, so every hyperhive host is a host that
# writes under hive-c0re's own state directory. # may run one.
{ {
pkgs, pkgs,
lib, lib,
@ -74,8 +74,7 @@ let
# one agreement. # one agreement.
# #
# `hiveName` has no fallback here for the reason ./glue-bao-tls.nix gives at # `hiveName` has no fallback here for the reason ./glue-bao-tls.nix gives at
# its own use of it: it is asserted set wherever # its own use of it: it is asserted set for every hyperhive host.
# `deploy.hive-controller.enable` is.
credentialPath = "secret/swarm/hives/${hyperhiveCfg.hiveName}/queue/agent"; credentialPath = "secret/swarm/hives/${hyperhiveCfg.hiveName}/queue/agent";
in in
{ {
@ -111,11 +110,11 @@ in
# #
# Shaped after ./swarm-grafana.nix's `haveClientIdentity` assertion — the # Shaped after ./swarm-grafana.nix's `haveClientIdentity` assertion — the
# same refusal, named to this principal's own pair. Where Grafana's gate # same refusal, named to this principal's own pair. Where Grafana's gate
# is `deploy.grafana.enable`, this reader's is the hive's own: every hive # is `deploy.grafana.enable`, this reader has no toggle of its own to
# runs one for its own agents, so on a hive, reading the store at all is # check: every hive runs one for its own agents, so reading the store at
# what asks for it. Hence `hiveReaderIdentity` — a host holding no hive # all is what asks for it. Hence `hiveReaderIdentity` alone — a host
# leaf has no store to read and nothing is missing. # holding no hive leaf has no store to read and nothing is missing.
(lib.mkIf (deployCfg.hive-controller.enable && hiveReaderIdentity) { (lib.mkIf (hyperhiveCfg.enable && hiveReaderIdentity) {
assertions = [ assertions = [
{ {
assertion = haveClientIdentity; assertion = haveClientIdentity;
@ -150,7 +149,7 @@ in
]; ];
}) })
(lib.mkIf (deployCfg.hive-controller.enable && haveClientIdentity) { (lib.mkIf (hyperhiveCfg.enable && haveClientIdentity) {
# Same rule as the unit's own gate: this reader exists on any host holding # 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 # a client identity, which is not every host that runs the store, so the
# store's module cannot name it. # store's module cannot name it.

View file

@ -31,10 +31,12 @@ let
pkiDir = if haveMintedPki then builtins.dirOf baoDeploy.clientCertFile else null; pkiDir = if haveMintedPki then builtins.dirOf baoDeploy.clientCertFile else null;
in in
{ {
config = lib.mkIf (deployCfg.swarm-secret-publisher.enable && haveMintedPki) { config =
services.hyperhive.deploy.swarm-secret-publisher = { lib.mkIf (hyperhiveCfg.enable && deployCfg.swarm-secret-publisher.enable && haveMintedPki)
baoClientCertFile = lib.mkDefault "${pkiDir}/secret-publisher.pem"; {
baoClientKeyFile = lib.mkDefault "${pkiDir}/secret-publisher-key.pem"; services.hyperhive.deploy.swarm-secret-publisher = {
}; baoClientCertFile = lib.mkDefault "${pkiDir}/secret-publisher.pem";
}; baoClientKeyFile = lib.mkDefault "${pkiDir}/secret-publisher-key.pem";
};
};
} }

View file

@ -34,7 +34,7 @@ let
pkiDir = if haveMintedPki then builtins.dirOf baoDeploy.clientCertFile else null; pkiDir = if haveMintedPki then builtins.dirOf baoDeploy.clientCertFile else null;
in in
{ {
config = lib.mkIf haveMintedPki { config = lib.mkIf (hyperhiveCfg.enable && haveMintedPki) {
services.hyperhive.deploy.hive-controller.tls = { services.hyperhive.deploy.hive-controller.tls = {
baoClientCertFile = lib.mkDefault "${pkiDir}/services-issuer.pem"; baoClientCertFile = lib.mkDefault "${pkiDir}/services-issuer.pem";
baoClientKeyFile = lib.mkDefault "${pkiDir}/services-issuer-key.pem"; baoClientKeyFile = lib.mkDefault "${pkiDir}/services-issuer-key.pem";

View file

@ -29,7 +29,7 @@ let
baoCfg = hyperhiveCfg.swarm.bao; baoCfg = hyperhiveCfg.swarm.bao;
in in
{ {
config = lib.mkIf deployCfg.authelia.enable { config = lib.mkIf (hyperhiveCfg.enable && deployCfg.authelia.enable) {
# One declaration, two readers: `clientId` is a read-only option # One declaration, two readers: `clientId` is a read-only option
# ./swarm-bao.nix owns, and ./swarm-otel.nix builds this principal's # ./swarm-bao.nix owns, and ./swarm-otel.nix builds this principal's
# authenticator audience from the same option. # authenticator audience from the same option.

View file

@ -32,7 +32,7 @@ let
otelCfg = hyperhiveCfg.swarm.otel; otelCfg = hyperhiveCfg.swarm.otel;
in in
{ {
config = lib.mkIf deployCfg.authelia.enable { config = lib.mkIf (hyperhiveCfg.enable && deployCfg.authelia.enable) {
# One declaration, two readers: `clientId` and `audience` are read-only # One declaration, two readers: `clientId` and `audience` are read-only
# options ./swarm-otel.nix derives from the scrape/push targets it owns, # options ./swarm-otel.nix derives from the scrape/push targets it owns,
# so this file states neither formula a second time. # so this file states neither formula a second time.

View file

@ -153,11 +153,11 @@ in
name = "hive-c0re"; name = "hive-c0re";
consumers = [ "hive-c0re" ]; consumers = [ "hive-c0re" ];
hostUnit = true; hostUnit = true;
enable = config.services.hyperhive.deploy.hive-controller.enable; enable = cfg.enable;
}) })
]; ];
config = lib.mkIf config.services.hyperhive.deploy.hive-controller.enable { config = lib.mkIf cfg.enable {
# This module is the host's only knowledge that agent containers # This module is the host's only knowledge that agent containers
# exist: they hang off the bridge, resolve through dnsmasq, and their # exist: they hang off the bridge, resolve through dnsmasq, and their
# UIs — plus the operator dashboard — are served by the gateway. # UIs — plus the operator dashboard — are served by the gateway.

View file

@ -7,10 +7,17 @@
{ {
pkgs, pkgs,
lib, lib,
config,
... ...
}: }:
{ {
options.services.hyperhive.c0re = { 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 { package = lib.mkOption {
type = lib.types.package; type = lib.types.package;
defaultText = lib.literalExpression "hyperhive.packages.\${system}.default"; defaultText = lib.literalExpression "hyperhive.packages.\${system}.default";

View file

@ -459,7 +459,7 @@ in
}; };
}; };
config = lib.mkIf deployCfg.forgejo.enable { config = lib.mkIf (config.services.hyperhive.enable && deployCfg.forgejo.enable) {
# Same principle as the vhost below — this service's own surface lives # Same principle as the vhost below — this service's own surface lives
# with the service. The SSO source is named alongside forgejo because # with the service. The SSO source is named alongside forgejo because
# its failure mode is a login that silently falls back, not an error. # its failure mode is a login that silently falls back, not an error.

View file

@ -86,8 +86,7 @@ let
# saying so, as the token path below. # saying so, as the token path below.
# #
# `hiveName` has no fallback here for the reason ./glue-matrix-bao-token.nix # `hiveName` has no fallback here for the reason ./glue-matrix-bao-token.nix
# gives at its own use of it: it is asserted set on every host that runs the # gives at its own use of it: it is asserted set for every hyperhive host.
# homeserver.
hiveLocalpart = "hive-${toString config.services.hyperhive.hiveName}"; hiveLocalpart = "hive-${toString config.services.hyperhive.hiveName}";
# The `as_token`, and the `hs_token` the spec requires alongside it. Both # The `as_token`, and the `hs_token` the spec requires alongside it. Both

View file

@ -35,7 +35,7 @@ in
imports = [ imports = [
(lib.mkRemovedOptionModule [ "services" "hyperhive" "network" "isolateContainers" ] '' (lib.mkRemovedOptionModule [ "services" "hyperhive" "network" "isolateContainers" ] ''
Network isolation is the only mode and is always on whenever Network isolation is the only mode and is always on whenever
the hive runs; the shared-netns path was removed. Remove hyperhive is enabled; the shared-netns path was removed. Remove
the setting. the setting.
'') '')
(lib.mkRemovedOptionModule [ "services" "hyperhive" "network" "upstreamDns" ] '' (lib.mkRemovedOptionModule [ "services" "hyperhive" "network" "upstreamDns" ] ''
@ -148,12 +148,11 @@ in
}; };
config = lib.mkMerge [ config = lib.mkMerge [
# Identity checks belong to the hive being on, not to the bridge # Identity checks belong to hyperhive being on, not to the bridge
# being up: a hive with no domain is misconfigured either way, and # being up: a hive with no domain is misconfigured either way, and
# moving them under the bridge gate would hide them on exactly the # moving them under the bridge gate would hide them on exactly the
# hosts that are hardest to debug. `hiveName` is asserted in # hosts that are hardest to debug.
# ./hyperhive.nix, where more than the hive depends on it. (lib.mkIf config.services.hyperhive.enable {
(lib.mkIf config.services.hyperhive.deploy.hive-controller.enable {
# This message is only useful if an operator can actually reach # This message is only useful if an operator can actually reach
# it, and an assertion competes with every eager default that # it, and an assertion competes with every eager default that
# reads the value it guards: option defaults that interpolate the # reads the value it guards: option defaults that interpolate the
@ -197,6 +196,16 @@ in
`services.hyperhive.domain` for you. `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
(`<hiveName>.<swarm.domain>`), not only a display name.
Set it (`services.hyperhive.hiveName = "pr1ma";`).
'';
}
]; ];
}) })

View file

@ -19,7 +19,7 @@ let
''; '';
in in
{ {
config = lib.mkIf config.services.hyperhive.deploy.hive-controller.enable { config = lib.mkIf cfg.enable {
# Socket unit for hive-priv — the narrow root helper that executes # Socket unit for hive-priv — the narrow root helper that executes
# privileged operations on behalf of hive-c0re. Systemd creates and # privileged operations on behalf of hive-c0re. Systemd creates and
# holds `/run/hive/priv.sock` before the first connection arrives. # holds `/run/hive/priv.sock` before the first connection arrives.

View file

@ -37,11 +37,12 @@ let
# The host-managed hive CA is the trust anchor for self-signed mode. # 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 # It is only stood up when the gateway actually serves a self-signed
# cert: the gateway must run here and be in self-signed mode. The # cert: the gateway must be in self-signed mode. `domain` is required
# self-signed condition is the gateway module's single source of truth # (asserted in hive-network.nix), so the leaf SANs always have a
# (`gateway.useSelfSigned`): true when neither an operator cert # domain to derive from. The self-signed condition is the gateway
# (`tls.certDir`) nor ACME is set. # module's single source of truth (`gateway.useSelfSigned`): true when
active = gatewayCfg.enable && gatewayCfg.useSelfSigned; # neither an operator cert (`tls.certDir`) nor ACME is set.
active = hyperhiveCfg.enable && gatewayCfg.useSelfSigned;
# How this hive's CA comes into existence when it is missing — and it # 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 # is one of exactly two things, chosen by config rather than by what
@ -1021,14 +1022,7 @@ in
# bundle. It is the ANCHOR bundle rather than `ca.pem` for the reason # 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 # spelled out where the bundle is written above; no key path is ever
# exposed (an agent that could read one could mint trusted certs). # 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 # 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 # they mint a token from authelia over TLS. Declared HERE, beside the
@ -1044,11 +1038,18 @@ in
# variable name. The anchor was never missing; nothing pointed the queue # variable name. The anchor was never missing; nothing pointed the queue
# client at it. # client at it.
# #
# Where there is no hive CA this module contributes nothing at all, and # Set unconditionally within this module's `active` guard, exactly like
# the clients then fall back to the platform roots — which is correct for # the line above: where there is no hive CA this module contributes
# a swarm fronted by a public certificate. # nothing at all, and the clients then fall back to the platform roots —
systemd.services.hive-c0re.environment.HIVE_C0RE_OIDC_CA_FILE = # which is correct for a swarm fronted by a public certificate.
lib.mkIf hyperhiveCfg.deploy.hive-controller.enable "${cfg.stateDir}/trust-bundle.pem"; 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.
systemd.services.swarm-controller.environment.SWARM_CONTROLLER_OIDC_CA_FILE = systemd.services.swarm-controller.environment.SWARM_CONTROLLER_OIDC_CA_FILE =
lib.mkIf hyperhiveCfg.deploy.swarm-controller.enable "${cfg.stateDir}/trust-bundle.pem"; lib.mkIf hyperhiveCfg.deploy.swarm-controller.enable "${cfg.stateDir}/trust-bundle.pem";
}; };

View file

@ -1,6 +1,7 @@
# Top-level, cross-cutting hyperhive options: the hive's identity # Top-level, cross-cutting hyperhive options: the master enable
# (domain + display names) and hive-wide feature toggles read by several # switch, the hive's identity (domain + display names), and hive-wide
# subsystem modules. Imported by the ./default.nix aggregator. # feature toggles read by several subsystem modules. Imported by the
# ./default.nix aggregator.
{ {
lib, lib,
config, config,
@ -21,9 +22,13 @@ 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 # Canonical hive DNS domain shared by every subsystem that needs a
# stable hostname. Typed nullOr (default null) so the option always # stable hostname. Typed nullOr (default null) so the option always
# exists, but it's REQUIRED wherever this host runs a hive — an # exists, but it's REQUIRED whenever hyperhive is enabled — an
# assertion in hive-network.nix fails eval when it's unset, since # 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 # matrix bakes it in on first boot and the gateway/forge/agent URLs all
# derive from it (no safe default). Full identity-surface # derive from it (no safe default). Full identity-surface
@ -56,8 +61,7 @@ in
stable name (currently: `services.hyperhive.swarm.matrix.serverName` stable name (currently: `services.hyperhive.swarm.matrix.serverName`
derives from this, defaulting to derives from this, defaulting to
`matrix.''${services.hyperhive.domain}` when `serverName` is `matrix.''${services.hyperhive.domain}` when `serverName` is
null). **Required** when null). **Required** when `services.hyperhive.enable` — eval fails
`services.hyperhive.deploy.hive-controller.enable` — eval fails
with a helpful message if it's unset (it's baked into matrix on 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 first boot and drives the gateway/forge/agent URLs, with no safe
default; changing it later is destructive). Exposed to agents as default; changing it later is destructive). Exposed to agents as
@ -84,22 +88,24 @@ in
# nothing — measured, after this warning fired on the conventional # nothing — measured, after this warning fired on the conventional
# case. `mkOptionDefault` is priority 1500, so anything lower is a # case. `mkOptionDefault` is priority 1500, so anything lower is a
# definition someone actually wrote (100 plain, 1000 mkDefault). # definition someone actually wrote (100 plain, 1000 mkDefault).
config.warnings = lib.optional (options.services.hyperhive.domain.highestPrio < 1500) '' config.warnings =
services.hyperhive.domain is set explicitly and is deprecated. This lib.optional (hiveCfg.enable && options.services.hyperhive.domain.highestPrio < 1500)
hive's address belongs in the swarm directory, which every hive in ''
the swarm shares a copy of: 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 (Or drop the value entirely if it is the conventional
`<hiveName>.<swarm.domain>` — that is the directory entry's own `<hiveName>.<swarm.domain>` — that is the directory entry's own
default.) default.)
Setting it here still wins, so nothing is broken right now. What 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 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 out of their copy of the directory, so a value written only here
leaves them pointing somewhere else with nothing detecting it. leaves them pointing somewhere else with nothing detecting it.
''; '';
# Where the swarm lives. Declared beside the hive's own identity # Where the swarm lives. Declared beside the hive's own identity
# because it is what that identity is derived FROM — every hive in a # because it is what that identity is derived FROM — every hive in a
@ -117,8 +123,7 @@ 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** when **Required** when `services.hyperhive.enable`, and deliberately
`services.hyperhive.deploy.hive-controller.enable`, and deliberately
not defaulted: there is no fallback worth having. A guessed not defaulted: there is no fallback worth having. A guessed
swarm domain is a wrong hostname that evaluates cleanly and swarm domain is a wrong hostname that evaluates cleanly and
deploys, which is worse than an eval failure telling an deploys, which is worse than an eval failure telling an
@ -139,10 +144,7 @@ in
example = "pr1ma"; example = "pr1ma";
description = '' description = ''
Human-readable name of this single-host hive instance. Human-readable name of this single-host hive instance.
**Required** when this host runs a hive, the swarm's secret store **Required** when `services.hyperhive.enable`. Distinct from
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) `services.hyperhive.domain` (the machine-addressable DNS name)
but no longer merely cosmetic: a hive occupies but no longer merely cosmetic: a hive occupies
`<hiveName>.<swarm.domain>`, so this is the label the hive is `<hiveName>.<swarm.domain>`, so this is the label the hive is
@ -152,28 +154,6 @@ 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/<name>` 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 (`<hiveName>.<swarm.domain>`), not only a
display name. Set it (`services.hyperhive.hiveName = "pr1ma";`).
'';
}
];
# The one hive-level option that describes something ABOVE the hive, # The one hive-level option that describes something ABOVE the hive,
# which is why it sits under `swarm` with the swarm-global services # which is why it sits under `swarm` with the swarm-global services
# rather than beside `hiveName`. # rather than beside `hiveName`.

View file

@ -919,7 +919,7 @@ in
}; };
}; };
config = lib.mkIf deployCfg.authelia.enable { config = lib.mkIf (hyperhiveCfg.enable && deployCfg.authelia.enable) {
# The derived half of the client list, declared the same way an # The derived half of the client list, declared the same way an
# operator declares one. Everything downstream then reads a single # operator declares one. Everything downstream then reads a single
# uniformly-typed `cfg.oidc.clients` and cannot tell the parts apart — # uniformly-typed `cfg.oidc.clients` and cannot tell the parts apart —

View file

@ -1573,7 +1573,7 @@ in
# they still fire when the cert paths are unset — the arm below is not # 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 # evaluated in that case, and an assertion that disappears exactly when
# its subject is broken would be worse than none. # its subject is broken would be worse than none.
(lib.mkIf deployCfg.bao.enable { (lib.mkIf (hyperhiveCfg.enable && deployCfg.bao.enable) {
assertions = [ assertions = [
{ {
assertion = lib.versionOlder baoDeploy.package.version "2.7.0"; assertion = lib.versionOlder baoDeploy.package.version "2.7.0";
@ -1655,7 +1655,7 @@ in
services.hyperhive.network.enable = lib.mkDefault true; services.hyperhive.network.enable = lib.mkDefault true;
}) })
(lib.mkIf deployCfg.bao.enable { (lib.mkIf (hyperhiveCfg.enable && deployCfg.bao.enable) {
# The wrapper alone, never `baoDeploy.package`: openbao's own module # The wrapper alone, never `baoDeploy.package`: openbao's own module
# installs the CLI *inside* the container, and this host had none at all, # 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 # so an operator either hopped into the container or re-typed the
@ -1845,7 +1845,7 @@ in
}; };
}) })
(lib.mkIf (deployCfg.bao.enable && haveServerTls) { (lib.mkIf (hyperhiveCfg.enable && deployCfg.bao.enable && haveServerTls) {
# Provisions the TPM-backed token the seal above names. One-shot and # Provisions the TPM-backed token the seal above names. One-shot and
# idempotent on ABSENCE, never on content: regenerating a PIN would # idempotent on ABSENCE, never on content: regenerating a PIN would
# orphan an already-sealed store, so a rebuild must not rotate it. # orphan an already-sealed store, so a rebuild must not rotate it.

View file

@ -106,7 +106,7 @@ in
}; };
}; };
config = lib.mkIf cfg.autoConfigure { config = lib.mkIf (hyperhiveCfg.enable && cfg.autoConfigure) {
# A CA that fails to issue is invisible until something makes a TLS call # 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. # hours later, so this oneshot is worth more than most services.
services.hyperhive.swarm.otel.journaldUnits = [ "swarm-ca" ]; services.hyperhive.swarm.otel.journaldUnits = [ "swarm-ca" ];

View file

@ -349,7 +349,7 @@ in
# `enable` moved to `services.hyperhive.deploy.swarm-controller.enable` — see # `enable` moved to `services.hyperhive.deploy.swarm-controller.enable` — see
# ./deploy.nix. `services.hyperhive.deploy.singleHostSwarm` still # ./deploy.nix. `services.hyperhive.deploy.singleHostSwarm` still
# asserts it, and that was never an exception to "not derived from # asserts it, and that was never an exception to "not derived from
# deploy.hive-controller.enable": that mode says "this box is the whole # services.hyperhive.enable": that mode says "this box is the whole
# deployment", which answers the topology question outright, where # deployment", which answers the topology question outright, where
# `enable` alone never can. Both packages moved to that same # `enable` alone never can. Both packages moved to that same
# `deploy.swarm-controller` block. What stays here is what the daemon IS. # `deploy.swarm-controller` block. What stays here is what the daemon IS.

View file

@ -405,7 +405,7 @@ in
}; };
}; };
config = lib.mkIf deployCfg.grafana.enable { config = lib.mkIf (hyperhiveCfg.enable && deployCfg.grafana.enable) {
# The gateway name and the quick-link, both inside `deploy.grafana` — that # 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 # 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 # exists, but only the host that RUNS it may claim the name; a client

View file

@ -47,7 +47,7 @@ let
# Both halves have to be here: the mint (authelia, for the plaintext) and an # Both halves have to be here: the mint (authelia, for the plaintext) and an
# identity (for the store). Neither implies the other. # identity (for the store). Neither implies the other.
active = cfg.enable && deployCfg.authelia.enable && haveClientIdentity; active = hyperhiveCfg.enable && cfg.enable && deployCfg.authelia.enable && haveClientIdentity;
hiveNames = lib.attrNames hyperhiveCfg.swarm.hives; hiveNames = lib.attrNames hyperhiveCfg.swarm.hives;

View file

@ -134,7 +134,7 @@ in
''; '';
}; };
config = lib.mkIf deployCfg.victorialogs.enable { config = lib.mkIf (hyperhiveCfg.enable && deployCfg.victorialogs.enable) {
# This store publishes its own health as prometheus metrics on the same # 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 # 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. # no exporter and no extra port — same arrangement as the metrics store.

View file

@ -95,7 +95,7 @@ in
''; '';
}; };
config = lib.mkIf deployCfg.victoriametrics.enable { config = lib.mkIf (hyperhiveCfg.enable && deployCfg.victoriametrics.enable) {
# The gateway name and the quick-link, both inside `deployCfg.victoriametrics.enable` — that # 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 # 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 # store exists, but only the host that RUNS it may claim the name; a

View file

@ -84,11 +84,13 @@
}; };
}; };
# Gated on the mesh itself, NOT on `deploy.hive-controller.enable`. The # Gated on the mesh itself, NOT on the c0re daemon. The mesh is host
# mesh is host networking, not a c0re feature: a swarm host that runs no # networking, not a c0re feature: a swarm host that runs no hive —
# hive — the snapshot store, for one — still has to join the mesh. # the snapshot store, for one — still has to join the mesh, and under
# Nothing below is c0re-specific; the peer data c0re consumes # the old `c0re.enable` gate it silently got no `wg-hive` interface
# (HIVE_PEER_CA_PATHS) is rendered in ./hive-c0re and stays gated there. # 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.
config = lib.mkIf config.services.hyperhive.deploy.wireguard.enable { config = lib.mkIf config.services.hyperhive.deploy.wireguard.enable {
assertions = [ assertions = [
{ {

View file

@ -70,8 +70,7 @@ let
# The same omission on a hive that does NOT run a homeserver. Separates the # 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 # matrix refusal's `deploy.matrix.enable` clause from the queue refusal, which
# checks only the hive's own toggle — an arm below reads exactly one refusal # has no toggle to check — an arm below reads exactly one refusal off it.
# off it.
remoteReaderNoMatrixMissingLeaves = hive { remoteReaderNoMatrixMissingLeaves = hive {
deploy.bao.clientCertFile = "/etc/pki/bao-client.pem"; deploy.bao.clientCertFile = "/etc/pki/bao-client.pem";
deploy.bao.clientKeyFile = "/etc/pki/bao-client-key.pem"; deploy.bao.clientKeyFile = "/etc/pki/bao-client-key.pem";

View file

@ -30,17 +30,17 @@ let
bare = hive { }; bare = hive { };
# The same stub with the central toggle (`deploy.hive-controller.enable`) # The same stub with the central toggle off. Paired with `bare` below to pin
# off. Paired with `bare` below to pin the defaults that do not read it: # the defaults that used to read `services.hyperhive.enable` and no longer
# each is asserted to hold the SAME literal in both, so a future edit # do: each is asserted to hold the SAME literal in both, so a future edit
# that quietly introduces the dependency — or that changes what the # that quietly re-introduces the dependency — or that changes what the
# default renders for a hive with the toggle on — fails here. Reading an # 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 # 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 # toggle being off costs nothing. Also the "installs the modules and turns
# nothing on" host the swarm-service absences below read: none of the # nothing on" host the swarm-service absences below read: none of the
# per-service deployment toggles derives from the hive being on, so it # per-service deployment toggles derives from the hive being on, so it
# renders the same absences `bare` does. # renders the same absences `bare` does.
centralToggleOff = hive { deploy.hive-controller.enable = false; }; centralToggleOff = hive { enable = false; };
# Every hive defaults the agents' queue address to the queue's name, so the # 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. # 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"; name = "the swarm controller's forgeTokenFile default does not consult the central toggle";
ok = ok =
(hive { (hive {
deploy.hive-controller.enable = false; enable = false;
deploy.forgejo.enable = true; deploy.forgejo.enable = true;
}).services.hyperhive.deploy.swarm-controller.forgeTokenFile }).services.hyperhive.deploy.swarm-controller.forgeTokenFile
== "/var/lib/hyperhive-forge/swarm-controller.token"; == "/var/lib/hyperhive-forge/swarm-controller.token";
@ -205,16 +205,15 @@ let
} }
{ {
# A leaf is a key that can SERVE every name in it, so the names it # A leaf is a key that can SERVE every name in it, so the names it
# asks for are that key's blast radius. The forge's host fronts forge # asks for are that key's blast radius. `bare` fronts forge and
# and nothing else; the last conjunct is the control, since # nothing else; the last conjunct is the control, since `auth.t.local`
# `auth.t.local` is in the swarm's set and an unnarrowed request would # is in the swarm's set and an unnarrowed request would carry it here
# carry it here too. # too.
name = "a gateway's services leaf asks for only the swarm names that host fronts"; name = "a gateway's services leaf asks for only the swarm names that host fronts";
ok = ok =
let let
forgeHere = hive { deploy.forgejo.enable = true; }; s = bare.services.hyperhive.swarm.serviceDomains;
s = forgeHere.services.hyperhive.swarm.serviceDomains; u = bare.systemd.services.swarm-services-cert.script;
u = forgeHere.systemd.services.swarm-services-cert.script;
in in
lib.hasInfix "alt_names=forge.t.local" u lib.hasInfix "alt_names=forge.t.local" u
&& !(lib.hasInfix "auth.t.local" u) && !(lib.hasInfix "auth.t.local" u)
@ -454,55 +453,6 @@ let
&& bare.services.dnsmasq.enable && bare.services.dnsmasq.enable
&& bare.networking.bridges ? hive-br0; && 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 in
runGroup "core-toggle" cases runGroup "core-toggle" cases

View file

@ -59,7 +59,7 @@ let
# just one level down. `recursiveUpdate` merges nested attrsets # just one level down. `recursiveUpdate` merges nested attrsets
# all the way down instead. # all the way down instead.
services.hyperhive = lib.recursiveUpdate { services.hyperhive = lib.recursiveUpdate {
deploy.hive-controller.enable = true; enable = true;
hiveName = "h1"; hiveName = "h1";
swarm.domain = "t.local"; swarm.domain = "t.local";
swarm.hives.h1.domain = "h1.t.local"; swarm.hives.h1.domain = "h1.t.local";

View file

@ -23,37 +23,20 @@ let
swarmServiceEnables swarmServiceEnables
; ;
# A dedicated services host with hives elsewhere: the swarm-services # The swarm-services toggle with the central one off, so the only thing
# toggle and no hive, the recipe docs/swarm/services.md gives. # that can enable the gateway/resolver/bridge here is that toggle's own
# module — every other module that asserts them is behind `enable`.
swarmServicesOnly = hive { swarmServicesOnly = hive {
deploy.hive-controller.enable = false; enable = false;
deploy.allSwarmServices = true; 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 # 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 # 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 # hive is running, and an agent reaching a host service is a claim about
# the host's own listeners either way. # the host's own listeners either way.
exposedPortsNoHive = hive { exposedPortsNoHive = hive {
deploy.hive-controller.enable = false; enable = false;
network.exposeHostPorts = [ 5432 ]; network.exposeHostPorts = [ 5432 ];
}; };
@ -146,44 +129,9 @@ let
# module rather than from any of their defaults. # module rather than from any of their defaults.
name = "the swarm-services toggle turns on the gateway, resolver and bridge by itself"; name = "the swarm-services toggle turns on the gateway, resolver and bridge by itself";
ok = ok =
swarmServicesSwitchAlone.services.hyperhive.gateway.enable swarmServicesOnly.services.hyperhive.gateway.enable
&& swarmServicesSwitchAlone.services.hyperhive.gateway.dns.enable && swarmServicesOnly.services.hyperhive.gateway.dns.enable
&& swarmServicesSwitchAlone.services.hyperhive.network.enable; && swarmServicesOnly.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, # An operator's explicit `false` beats every `mkDefault` assertion,

View file

@ -5,9 +5,8 @@ owns what is true _across_ hives — so a swarm runs one of them and most hives
leave it off. leave it off.
Opt-in per host via `services.hyperhive.deploy.swarm-controller.enable`, which is Opt-in per host via `services.hyperhive.deploy.swarm-controller.enable`, which is
deliberately **not** derived from deliberately **not** derived from `services.hyperhive.enable`: turning it on is
`services.hyperhive.deploy.hive-controller.enable`: turning it on is a statement a statement about swarm topology, not about whether hyperhive is installed.
about swarm topology, not about whether this host runs a hive.
## What it does today ## What it does today