Watch
0
0
Fork
You've already forked hyperhive
0

move hiveName to deploy.hive-controller; only a hive host needs a name

This commit is contained in:
atlas 2026-10-26 16:06:11 +01:00 • committed by mara
commit 0c6e8fc5a2
37 changed files with 402 additions and 215 deletions

View file

@ -143,6 +143,12 @@ in
#
# The runner moved from `deploy.forgejo.ci` to `deploy.ci-runner` with
# every option keeping its type and meaning, so the old paths forward.
# `hiveName` names the hive a host runs, so it moved under the service
# that runs one. Same type and meaning.
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "hiveName" ]
[ "services" "hyperhive" "deploy" "hive-controller" "hiveName" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "deploy" "forgejo" "ci" "enable" ]
[ "services" "hyperhive" "deploy" "ci-runner" "enable" ]
@ -514,6 +520,39 @@ in
'';
};
hive-controller.hiveName = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "pr1ma";
description = ''
Name of the hive this host runs, and the key of its entry in
{option}`services.hyperhive.swarm.hives`. **Required** when
{option}`services.hyperhive.deploy.hive-controller.enable` is set;
a host that runs no hive needs none.
Not merely a display name: the hive occupies
`<hiveName>.<swarm.domain>`, and the name is the hive's identity to
the swarm (its secret-store certificate subject, its OIDC client, its
status key). Distinct from {option}`services.hyperhive.domain`, the
machine-addressable DNS name. Exposed to agents as
`HYPERHIVE_HIVE_NAME`; surfaced in the dashboard chrome and per-agent
system prompt. The identity surfaces are listed in
`docs/process/conventions.md`, § Hive identity.
'';
};
hive-controller.dashboardDomain = lib.mkOption {
type = lib.types.nullOr lib.types.str;
readOnly = true;
internal = true;
description = ''
Read-only: this hive's entry in
{option}`services.hyperhive.swarm.dashboardDomains`, or null where
`hiveName` has no entry in {option}`services.hyperhive.swarm.hives`
(asserted).
'';
};
swarm-controller.enable = lib.mkOption {
type = lib.types.bool;
default = false;

View file

@ -32,7 +32,7 @@ let
hiveOtelSecret = import ./lib/hive-otel-secret.nix {
inherit (hyperhiveCfg.swarm.authelia) hiveClientPrefix;
inherit (hyperhiveCfg) hiveName;
inherit (hyperhiveCfg.deploy.hive-controller) hiveName;
};
havePair = cert: key: cert != null && key != null;
@ -47,7 +47,7 @@ let
# under the hive's own leaf.
swarm-bao-hive-otel-oidc =
otel.collector.enable
&& hyperhiveCfg.hiveName != null
&& hyperhiveCfg.deploy.hive-controller.hiveName != null
&& havePair baoDeploy.clientCertFile baoDeploy.clientKeyFile
&& otel.clientSecretFile == hiveOtelSecret.path;
# ./swarm-grafana.nix

View file

@ -41,16 +41,18 @@ let
# re-issuing every client certificate in the swarm.
pkiDir = import ./lib/bao-pki-dir.nix;
# 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 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;
# The hive this host runs, if any, and what it 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. `hiveName` is asserted set wherever
# `deploy.hive-controller.enable` is (./hyperhive.nix), and the reader leaf
# below exists only there: a store host that runs no hive has no hive to
# name, and a fallback CN would read as a second supported spelling.
runsHive = deployCfg.hive-controller.enable;
clientCn = hyperhiveCfg.deploy.hive-controller.hiveName;
# Every other hive in the swarm directory. This host's own hive is signed
# into the top of the directory, beside the leaves its units read.
# Every other hive in the swarm directory. This host's own hive, where there
# is one, is signed into the top of the directory, beside the leaves its
# units read.
otherHives = lib.filter (h: h != clientCn) (lib.attrNames hyperhiveCfg.swarm.hives);
# $1 dir $2 basename $3 CN $4 SAN or "" $5 EKU
@ -83,15 +85,16 @@ in
serverCertFile = lib.mkDefault "${pkiDir}/server.pem";
serverKeyFile = lib.mkDefault "${pkiDir}/server-key.pem";
clientCaFile = lib.mkDefault "${pkiDir}/ca.pem";
# A reader on this host, which happens to be the host that mints. Only
# these three are what a reader elsewhere needs placed by hand; that they
# collapse to the same CA file here is a property of self-signing, not of
# the pairing.
clientCertFile = lib.mkDefault "${pkiDir}/client.pem";
clientKeyFile = lib.mkDefault "${pkiDir}/client-key.pem";
serverCaFile = lib.mkDefault "${pkiDir}/ca.pem";
# The hive's own reader on this host, which happens to be the host that
# mints. Only where a hive runs: the leaf's CN is the hive's name, and a
# store host without a hive has no reader to name. That the pair collapses
# to the same CA file as the server's here is a property of
# self-signing, not of the pairing.
clientCertFile = lib.mkIf runsHive (lib.mkDefault "${pkiDir}/client.pem");
clientKeyFile = lib.mkIf runsHive (lib.mkDefault "${pkiDir}/client-key.pem");
# 🩸 The readers below each present a leaf of their own rather than
# `client.pem` above. The leaf is what bao sees, so this pairing is the
# whole of what turns "one principal with the union of their grants" into
@ -181,8 +184,10 @@ in
# CA is a file rather than a service.
[ -s ${pkiDir}/server.pem ] || ${signLeaf} ${pkiDir} server \
${lib.escapeShellArg cfg.domain} ${lib.escapeShellArg "DNS:${cfg.domain}"} serverAuth
[ -s ${pkiDir}/client.pem ] || ${signLeaf} ${pkiDir} client \
${lib.escapeShellArg clientCn} "" clientAuth
${lib.optionalString runsHive ''
[ -s ${pkiDir}/client.pem ] || ${signLeaf} ${pkiDir} client \
${lib.escapeShellArg clientCn} "" clientAuth
''}
# Minted whether or not a controller runs here, because the case it
# serves is the one where it does not: a controller elsewhere needs a
@ -329,7 +334,7 @@ in
[ -s ${pkiDir}/forge-publisher.pem ] || ${signLeaf} ${pkiDir} forge-publisher \
${lib.escapeShellArg deployCfg.bao.forgePublisherCommonName} "" clientAuth
${lib.optionalString deployCfg.hive-controller.enable ''
${lib.optionalString runsHive ''
# This hive's gateway-leaf identity, which opens
# `pki/issue/<hiveLeafPkiRolePrefix>-<hive>` and nothing else. Its
# subject carries this hive's name, because ./swarm-bao.nix writes

View file

@ -9,10 +9,10 @@
# second signer would duplicate that file's `openssl` helper to no benefit.
# What belongs here is the pairing: which paths this host's controller reads.
#
# ⚠️ Gated on the leaf existing, not on the store being enabled. A controller
# on the store's own host is one deployment; a controller three networks away
# holding a leaf issued out of band is another, and both want the same wiring.
# `deploy.bao.enable` would have made the first one the only supported shape.
# ⚠️ Gated on a PKI directory existing (the store runs here, or the operator
# named a reader leaf), not on the store alone. A controller on the store's own
# host is one deployment; a controller three networks away holding a leaf
# issued out of band is another, and both want the same wiring.
#
# Everything is `mkDefault`. An operator naming their own paths wins.
{
@ -25,11 +25,18 @@ let
deployCfg = hyperhiveCfg.deploy;
baoDeploy = deployCfg.bao;
# Where ./glue-bao-tls.nix puts the leaves. Derived from the reader's own
# path rather than repeating that file's directory literal: an operator who
# moves the PKI moves both, and the two cannot drift apart.
haveMintedPki = baoDeploy.clientCertFile != null;
pkiDir = if haveMintedPki then builtins.dirOf baoDeploy.clientCertFile else null;
# Where ./glue-bao-tls.nix puts the leaves. An operator's own `clientCertFile`
# names the directory when one is set (an operator who moves the PKI moves
# both, and the two cannot drift apart); on a store host that runs no hive
# there is no reader leaf to derive from, so it is the store's directory.
haveMintedPki = baoDeploy.clientCertFile != null || baoDeploy.enable;
pkiDir =
if baoDeploy.clientCertFile != null then
builtins.dirOf baoDeploy.clientCertFile
else if baoDeploy.enable then
import ./lib/bao-pki-dir.nix
else
null;
in
{
config = lib.mkIf (deployCfg.swarm-controller.enable && haveMintedPki) {

View file

@ -23,13 +23,13 @@ let
autheliaMachine = hyperhiveCfg.swarm.authelia.machine;
# ./otel.nix: `senderAuth`, `hiveLeaf`, `fetches`, `fetchedSecretPath`.
senderAuth = otel.clientSecretFile != null && hyperhiveCfg.hiveName != null;
senderAuth = otel.clientSecretFile != null && hyperhiveCfg.deploy.hive-controller.hiveName != null;
hiveLeaf =
hyperhiveCfg.deploy.bao.clientCertFile != null && hyperhiveCfg.deploy.bao.clientKeyFile != null;
fetchedSecretPath =
(import ./lib/hive-otel-secret.nix {
inherit (hyperhiveCfg.swarm.authelia) hiveClientPrefix;
inherit (hyperhiveCfg) hiveName;
inherit (hyperhiveCfg.deploy.hive-controller) hiveName;
}).path;
fetches = senderAuth && hiveLeaf && otel.clientSecretFile == fetchedSecretPath;
waits = senderAuth && !fetches;

View file

@ -30,7 +30,7 @@ let
matrixCfg.gatewayHost != null
&& hyperhiveCfg.deploy.hive-controller.enable
&& gatewayCfg.enable
&& hyperhiveCfg.swarm.dashboardDomain != null;
&& hyperhiveCfg.deploy.hive-controller.dashboardDomain != null;
# Same default as `effectiveServerName` in ./hive-matrix.nix.
serverName =

View file

@ -9,7 +9,7 @@
# belongs here is the pairing: which host paths go to the publisher in
# authelia's container.
#
# ⚠️ Gated on the leaf existing, not on the store being enabled — the same rule
# ⚠️ Gated on a PKI directory existing, not on the store alone — the same rule
# ./glue-controller-bao-identity.nix states. A publisher on the store's own
# host is one deployment; a publisher beside a remote authelia holding a leaf
# issued out of band is another, and both want this wiring.
@ -25,11 +25,18 @@ let
deployCfg = hyperhiveCfg.deploy;
baoDeploy = deployCfg.bao;
# Where ./glue-bao-tls.nix puts the leaves, derived from the reader's own
# path rather than repeating that file's directory literal: an operator who
# moves the PKI moves both, and the two cannot drift apart.
haveMintedPki = baoDeploy.clientCertFile != null;
pkiDir = if haveMintedPki then builtins.dirOf baoDeploy.clientCertFile else null;
# Where ./glue-bao-tls.nix puts the leaves. An operator's own `clientCertFile`
# names the directory when one is set (an operator who moves the PKI moves
# both, and the two cannot drift apart); on a store host that runs no hive
# there is no reader leaf to derive from, so it is the store's directory.
haveMintedPki = baoDeploy.clientCertFile != null || baoDeploy.enable;
pkiDir =
if baoDeploy.clientCertFile != null then
builtins.dirOf baoDeploy.clientCertFile
else if baoDeploy.enable then
import ./lib/bao-pki-dir.nix
else
null;
in
{
config = lib.mkIf (deployCfg.swarm-secret-publisher.enable && haveMintedPki) {

View file

@ -49,13 +49,13 @@ in
# required (asserted in hive-network.nix), so it's always set.
HYPERHIVE_HIVE_DOMAIN = config.services.hyperhive.domain;
}
// lib.optionalAttrs (config.services.hyperhive.swarm.dashboardDomain != null) {
// lib.optionalAttrs (config.services.hyperhive.deploy.hive-controller.dashboardDomain != null) {
# The host the dashboard and the agent UIs answer on, for the URLs c0re
# hands out (`hivectl open home`, each agent's UI link).
HYPERHIVE_DASHBOARD_DOMAIN = config.services.hyperhive.swarm.dashboardDomain;
HYPERHIVE_DASHBOARD_DOMAIN = config.services.hyperhive.deploy.hive-controller.dashboardDomain;
}
// lib.optionalAttrs (config.services.hyperhive.hiveName != null) {
HYPERHIVE_HIVE_NAME = config.services.hyperhive.hiveName;
// lib.optionalAttrs (config.services.hyperhive.deploy.hive-controller.hiveName != null) {
HYPERHIVE_HIVE_NAME = config.services.hyperhive.deploy.hive-controller.hiveName;
}
// lib.optionalAttrs (config.services.hyperhive.swarm.name != null) {
HYPERHIVE_SWARM_NAME = config.services.hyperhive.swarm.name;
@ -221,7 +221,7 @@ in
# The identity swarm-authelia.nix already declares for every entry in
# `swarm.hives` — the hive does not choose its own name here, it uses
# the one the roster gave it.
HIVE_C0RE_OIDC_CLIENT_ID = "hive-${config.services.hyperhive.hiveName}";
HIVE_C0RE_OIDC_CLIENT_ID = "hive-${config.services.hyperhive.deploy.hive-controller.hiveName}";
}
//
# Where the agents reach the queue: the address goes into every container in

View file

@ -1,5 +1,5 @@
# The hive's gateway surface, declared under `services.hyperhive.gateway.vhosts`
# like every service's: the dashboard's vhost (`swarm.dashboardDomain`), which
# like every service's: the dashboard's vhost (`deploy.hive-controller.dashboardDomain`), which
# carries the dashboard behind authelia, per-agent routing and the swagger UI;
# and the vhost named for the hive domain, which redirects everything to the
# dashboard's name. The matrix redirect, and discovery for a server name pinned to
@ -12,7 +12,7 @@
let
hyperhiveCfg = config.services.hyperhive;
gatewayCfg = hyperhiveCfg.gateway;
inherit (hyperhiveCfg.swarm) dashboardDomain;
inherit (hyperhiveCfg.deploy.hive-controller) dashboardDomain;
autheliaDomain = hyperhiveCfg.swarm.authelia.domain;
inherit (gatewayCfg.lib) errorPages;

View file

@ -706,10 +706,8 @@ in
# per-host flag is false exactly where this entry is needed.
webhook.ALLOWED_HOST_LIST = lib.concatStringsSep "," (
lib.unique (
[
"external"
hyperhiveDomain
]
[ "external" ]
++ lib.optional (hyperhiveDomain != null) hyperhiveDomain
++ lib.optional (swarmDomain != null) swarmDomain
)
);

View file

@ -423,7 +423,7 @@ in
# answer; a service added later lands here with no edit, and cannot
# land here with a different condition than it used for DNS.
networking.hosts = lib.mkIf cfg.localHostsEntry {
"127.0.0.1" = lib.unique ([ hyperhiveDomain ] ++ cfg.localNames);
"127.0.0.1" = lib.unique (lib.optional (hyperhiveDomain != null) hyperhiveDomain ++ cfg.localNames);
};
};
}

View file

@ -52,19 +52,18 @@
# Hive authoritative records — answer queries for the hive domain
# + its sub-domains with the bridge IP, where nginx is reachable
# from every container netns.
address = [
"/${hyperhiveDomain}/${networkCfg.bridgeIp}"
]
# Names contributed by the modules that own them
# (`gateway.localNames`). Same address as everything above — the
# bridge IP is the gateway's answer for anything it fronts, and a
# contributing module neither knows nor should know it.
#
# `unique` is not tidiness: two modules claiming one name would
# otherwise emit two `address=` rules for it, and dnsmasq resolves
# that by precedence rather than by complaining. An assertion in
# ./default.nix makes the collision loud instead.
++ map (name: "/${name}/${networkCfg.bridgeIp}") (lib.unique cfg.localNames);
address =
lib.optional (hyperhiveDomain != null) "/${hyperhiveDomain}/${networkCfg.bridgeIp}"
# Names contributed by the modules that own them
# (`gateway.localNames`). Same address as everything above — the
# bridge IP is the gateway's answer for anything it fronts, and a
# contributing module neither knows nor should know it.
#
# `unique` is not tidiness: two modules claiming one name would
# otherwise emit two `address=` rules for it, and dnsmasq resolves
# that by precedence rather than by complaining. An assertion in
# ./default.nix makes the collision loud instead.
++ map (name: "/${name}/${networkCfg.bridgeIp}") (lib.unique cfg.localNames);
# DHCP pool covering all usable host addresses on the bridge
# subnet — bounds computed by hive-network.nix from
# bridgeIp/bridgePrefixLength. All containers (agents and service

View file

@ -13,7 +13,7 @@ in
{
imports = [
(lib.mkRemovedOptionModule [ "services" "hyperhive" "gateway" "auth" ] ''
The dashboard is served on services.hyperhive.swarm.dashboardDomain
The dashboard is served on services.hyperhive.deploy.hive-controller.dashboardDomain
behind authelia, and only accounts in authelia's `admins` group can
open it. There is no HTTP Basic auth and no htpasswd file. Remove
the setting; add an operator with `swarmctl user add <name> --group admins`.

View file

@ -9,6 +9,8 @@ let
networkCfg = config.services.hyperhive.network;
tlsCfg = config.services.hyperhive.deploy.hive-controller.tls;
gatewayCfg = config.services.hyperhive.gateway;
# The hive's own domain; null on a homeserver host that runs no hive, where
# the client is pointed at the server name instead.
hyperhiveDomain = config.services.hyperhive.domain;
# Same runtime→build-time bridge hive-forge and swarm-ci-runner cross:
@ -491,11 +493,14 @@ in
};
# FluffyChat boot-config pre-fill so the client's
# `.well-known/matrix/client` lookup hits the right delegation
# endpoint. `domain` is required, so this is always present.
# endpoint. A hive host names its hive domain; a homeserver host
# without a hive has none and names the server name.
"= /config.json" = {
extraConfig = ''
default_type application/json;
return 200 '{"defaultHomeserver":"${hyperhiveDomain}"}';
return 200 '{"defaultHomeserver":"${
if hyperhiveDomain != null then hyperhiveDomain else effectiveServerName
}"}';
'';
};
};

View file

@ -219,7 +219,7 @@ in
which each hive occupies one sub-domain. A guessed value would
be a wrong hostname that evaluates cleanly and deploys. Set it
(`services.hyperhive.swarm.domain = "example.com";`) — with
`hiveName` it also derives `services.hyperhive.domain` for you.
`deploy.hive-controller.hiveName` it also derives `services.hyperhive.domain` for you.
'';
}
];

View file

@ -68,17 +68,20 @@ let
# goes on the swarm-services leaf below, and asking this role for one is a
# refusal.
hiveLeafRole =
if hyperhiveCfg.hiveName == null then
if hyperhiveCfg.deploy.hive-controller.hiveName == null then
null
else
"${baoDeploy.hiveLeafPkiRolePrefix}-${hyperhiveCfg.hiveName}";
"${baoDeploy.hiveLeafPkiRolePrefix}-${hyperhiveCfg.deploy.hive-controller.hiveName}";
# Everything after the first rides as a SAN.
hiveLeafNames = [
domain
"*.${domain}"
]
++ lib.optional (hyperhiveCfg.swarm.dashboardDomain != null) hyperhiveCfg.swarm.dashboardDomain;
hiveLeafNames =
lib.optionals (domain != null) [
domain
"*.${domain}"
]
++ lib.optional (
hyperhiveCfg.deploy.hive-controller.dashboardDomain != null
) hyperhiveCfg.deploy.hive-controller.dashboardDomain;
showPath = p: if p == null then "unset" else p;
@ -522,7 +525,7 @@ in
# even while its current leaf is still valid.
${
if hiveLeafRole == null then
''fail "services.hyperhive.hiveName is unset, so this hive has no gateway-leaf role to issue from."''
''fail "services.hyperhive.deploy.hive-controller.hiveName is unset, so this hive has no gateway-leaf role to issue from."''
else
requireHiveIdentity "ask the store for its gateway leaf"
}

View file

@ -9,6 +9,7 @@
}:
let
hiveCfg = config.services.hyperhive;
hiveName = hiveCfg.deploy.hive-controller.hiveName;
in
{
imports = [
@ -46,8 +47,8 @@ in
# would recreate the second path (and, since that default reads
# `swarm.domain` too, a value that can differ from the directory's).
default =
if hiveCfg.hiveName != null && hiveCfg.swarm.hives ? ${hiveCfg.hiveName} then
hiveCfg.swarm.hives.${hiveCfg.hiveName}.domain
if hiveName != null && hiveCfg.swarm.hives ? ${hiveName} then
hiveCfg.swarm.hives.${hiveName}.domain
else
null;
defaultText = lib.literalExpression "services.hyperhive.swarm.hives.\${hiveName}.domain, or null when there is no entry for this hive";
@ -90,7 +91,7 @@ in
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 hiveName}".domain = "${toString hiveCfg.domain}";
(Or drop the value entirely if it is the conventional
`<hiveName>.<swarm.domain>` — that is the directory entry's own
@ -115,7 +116,7 @@ in
occupies its own sub-domain of it, which is why every entry in
`services.hyperhive.swarm.hives` defaults its `domain` to
`<name>.<swarm.domain>` — set this plus
`services.hyperhive.hiveName`, list the hives by name, and no
`services.hyperhive.deploy.hive-controller.hiveName`, list the hives by name, and no
hive in the swarm states an address at all.
**Required** on every host that runs a hyperhive service (a
@ -129,56 +130,69 @@ in
'';
};
# Human display name for this hive. Distinct from the DNS domain
# above (machine-readable) — see docs/process/conventions.md::Hive identity
# for the domain-vs-name-vs-swarm distinction + the env-var
# propagation chain. The swarm's display name is
# `services.hyperhive.swarm.name`, one level out: this hive is named
# here, the swarm it belongs to is named there.
options.services.hyperhive.hiveName = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "pr1ma";
description = ''
Human-readable name of this single-host hive instance.
**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)
and not merely cosmetic: a hive occupies
`<hiveName>.<swarm.domain>`, so this is the label the hive is
*addressed* by as well as the one it is called. Exposed to
agents as `HYPERHIVE_HIVE_NAME`; surfaced in the dashboard
chrome and per-agent system prompt.
'';
};
# 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 queue credential reader fetches. A null there renders
# as an empty string that evaluates and deploys.
# The hive's name is `deploy.hive-controller.hiveName`, declared in
# ./deploy.nix beside the switch that makes a host run a hive. The
# identity surfaces (label, domain, display names, env-var chain) are in
# docs/process/conventions.md::Hive identity.
#
# A hive turns the name into an identifier with no fallback: hive-c0re's
# OIDC client and matrix localpart, the CN of the hive's `client.pem`
# (./glue-bao-tls.nix), and the `hives/<name>` path of its secrets. 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;
assertion = !hiveCfg.deploy.hive-controller.enable || 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";`).
hyperhive requires services.hyperhive.deploy.hive-controller.hiveName
to be set on a host that runs a hive — 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.deploy.hive-controller.hiveName = "pr1ma";`).
'';
}
{
# An EMPTY `hives` fires this too, deliberately: since
# `swarm.domain` is required, every hive is in a swarm — a swarm of
# one is still a swarm — so a directory with no entry for this host
# is missing one either way. It also has to fire here, because this
# host's own domain is read out of the directory: without the entry
# `services.hyperhive.domain` is null and the generic required-domain
# assertion in ./hive-network.nix would fire instead, naming an
# option the operator should not be setting.
#
# Guarded on `hiveName != null` so the required-hiveName assertion
# above is what fires for that case — two assertions naming the same
# missing value is noise.
assertion =
!hiveCfg.deploy.hive-controller.enable || hiveName == null || hiveCfg.swarm.hives ? ${hiveName};
message = ''
services.hyperhive.swarm.hives has no entry for this hive
(services.hyperhive.deploy.hive-controller.hiveName = "${toString hiveName}").
`hives` describes every hive in the swarm including this one,
so that every host can share one identical attrset, and this
hive's own domain is derived from its entry. Add:
services.hyperhive.swarm.hives."${toString hiveName}" = { };
No fields are needed: `domain` defaults to
`<name>.<swarm.domain>`. Set it in the entry if this hive is
addressed by something else.
Declared hives: ${lib.concatStringsSep ", " (lib.attrNames hiveCfg.swarm.hives)}
'';
}
];
config.services.hyperhive.deploy.hive-controller.dashboardDomain =
if hiveName != null && hiveCfg.swarm.dashboardDomains ? ${hiveName} then
hiveCfg.swarm.dashboardDomains.${hiveName}
else
null;
# 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`.
# rather than beside the hive's name.
options.services.hyperhive.swarm.name = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;

View file

@ -48,7 +48,7 @@ let
# Where `swarm-bao-hive-otel-oidc` lands the secret on this host.
hiveOtelSecret = import ./lib/hive-otel-secret.nix {
inherit (config.services.hyperhive.swarm.authelia) hiveClientPrefix;
inherit (config.services.hyperhive) hiveName;
inherit (config.services.hyperhive.deploy.hive-controller) hiveName;
};
fetchedSecretPath = hiveOtelSecret.path;
in
@ -190,7 +190,10 @@ in
clientSecretFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default =
if hiveLeaf && config.services.hyperhive.hiveName != null then fetchedSecretPath else null;
if hiveLeaf && config.services.hyperhive.deploy.hive-controller.hiveName != null then
fetchedSecretPath
else
null;
defaultText = lib.literalExpression ''"/run/hive-otel-oidc/''${swarm.authelia.hiveClientPrefix}''${hiveName}.secret" when deploy.bao.clientCertFile and clientKeyFile are set, else null'';
example = "/var/lib/secrets/hive-telemetry.secret";
description = ''
@ -256,7 +259,7 @@ in
deployCfg = config.services.hyperhive.deploy;
swarmOtelCfg = config.services.hyperhive.swarm.otel;
hiveOtelCfg = config.services.hyperhive.swarm.hiveOtel;
hiveName = config.services.hyperhive.hiveName;
hiveName = config.services.hyperhive.deploy.hive-controller.hiveName;
listen = "${config.services.hyperhive.network.bridgeIp}:${toString hiveOtelCfg.collectorPort}";
swarmName = "otlphttp/swarm";
gatewayCfg = config.services.hyperhive.gateway;
@ -884,7 +887,9 @@ in
services.hyperhive.otel.clientSecretFile = ${
if otel.clientSecretFile == null then "null" else otel.clientSecretFile
}
services.hyperhive.hiveName = ${if hiveName == null then "null" else hiveName}
services.hyperhive.deploy.hive-controller.hiveName = ${
if hiveName == null then "null" else hiveName
}
Every hive authenticates as itself — that is what makes the
`hive` label on its metrics mean anything — so both are

View file

@ -1310,9 +1310,10 @@ let
# same class of surprise it exists to remove.
baoCli =
let
# The reader's identity `glue-bao-tls` already mints on this host, so
# cert-auth login needs nothing typed either. A token stays the
# operator's to supply — a wrapper cannot conjure one.
# The hive reader identity `glue-bao-tls` mints where a hive runs, so
# cert-auth login needs nothing typed either. A store host without a hive
# has no such identity and the operator supplies a token or a leaf; a
# wrapper cannot conjure either.
wrapArgs = [
"--set-default BAO_ADDR ${lib.escapeShellArg "https://${cfg.domain}:${toString cfg.port}"}"
]

View file

@ -1,6 +1,6 @@
# The swarm-level web UI: a static bundle served by the gateway's nginx,
# behind authelia. Distinct from the per-hive dashboard (hive-c0re's, on
# `swarm.dashboardDomain`) — this one answers for the swarm apex and is the
# `deploy.hive-controller.dashboardDomain`) — this one answers for the swarm apex and is the
# operator's view across hives.
#
# Options, and the swarm apex's vhost, declared below through

View file

@ -118,7 +118,7 @@
# our own entry would configure a tunnel to ourselves.
meshPeers = lib.filterAttrs (
name: p:
name != config.services.hyperhive.hiveName
name != config.services.hyperhive.deploy.hive-controller.hiveName
&& p.wireguardPublicKey != null
&& p.wireguardAddress != null
) config.services.hyperhive.swarm.hives;

View file

@ -1,6 +1,6 @@
# The swarm's directory: one entry per hive, **including this one**,
# identical on every host in the swarm. `services.hyperhive.hiveName`
# says which entry is us.
# identical on every host in the swarm. A host that runs a hive says which
# entry is its own with `services.hyperhive.deploy.hive-controller.hiveName`.
#
# Why a directory rather than a per-host peer list: every field here is
# intrinsic to the hive it describes — none of them says anything about
@ -217,14 +217,14 @@ in
Every hive in this swarm, keyed by `hiveName` — **including this
host's own hive**. The same attrset is meant to be identical on
every host in the swarm, so it can be written once and shared;
`services.hyperhive.hiveName` is what makes a given host read it
`services.hyperhive.deploy.hive-controller.hiveName` is what makes a hive host read it
as "me and four others" rather than "five peers".
Each entry needs no fields at all in the conventional case: a
hive's `domain` defaults to `<name>.<swarm.domain>`, so the whole
directory is usually a list of names.
It must contain an entry for `hiveName`, which is asserted — this
On a host that runs a hive it must contain an entry for `hiveName`, which is asserted — this
host's own address is read out of it (it is where
`services.hyperhive.domain` derives from), and a hive that lists
everyone but itself would otherwise derive its own peer set as
@ -247,16 +247,6 @@ in
'';
};
options.services.hyperhive.swarm.dashboardDomain = lib.mkOption {
type = lib.types.nullOr lib.types.str;
readOnly = true;
internal = true;
description = ''
Read-only: this hive's entry in `dashboardDomains`, or null where
`hiveName` has no entry in `hives` (asserted below).
'';
};
options.services.hyperhive.swarm.serviceDomains = lib.mkOption {
type = lib.types.listOf lib.types.str;
readOnly = true;
@ -284,49 +274,11 @@ in
if swarmCfg.domain == null then "${name}.hives.invalid" else "${name}.hives.${swarmCfg.domain}"
) swarmCfg.hives;
services.hyperhive.swarm.dashboardDomain =
if cfg.hiveName != null && swarmCfg.dashboardDomains ? ${cfg.hiveName} then
swarmCfg.dashboardDomains.${cfg.hiveName}
else
null;
services.hyperhive.swarm.serviceDomains = lib.sort (a: b: a < b) (
lib.unique (lib.filter (d: d != null && d != "") serviceDomains')
);
assertions = [
{
# An EMPTY `hives` fires this too, deliberately: since
# `swarm.domain` is required, every hive is in a swarm — a
# swarm of one is still a swarm — so a directory with no entry
# for this host is missing one either way. It also has to fire
# here, because this host's own domain is now read out of the
# directory: without the entry `services.hyperhive.domain` is
# null and the generic required-domain assertion in
# ./hive-network.nix would fire instead, naming an option the
# operator should not be setting.
#
# Guarded on `hiveName != null` so the required-hiveName
# assertion in ./hyperhive.nix is what fires for that case —
# two assertions naming the same missing value is noise.
assertion = cfg.hiveName == null || swarmCfg.hives ? ${cfg.hiveName};
message = ''
services.hyperhive.swarm.hives has no entry for this hive
(services.hyperhive.hiveName = "${toString cfg.hiveName}").
`hives` describes every hive in the swarm including this one,
so that every host can share one identical attrset, and this
hive's own domain is derived from its entry. Add:
services.hyperhive.swarm.hives."${toString cfg.hiveName}" = { };
No fields are needed: `domain` defaults to
`<name>.<swarm.domain>`. Set it in the entry if this hive is
addressed by something else.
Declared hives: ${lib.concatStringsSep ", " (lib.attrNames swarmCfg.hives)}
'';
}
(nameGuards.mustBeIdent {
option = "services.hyperhive.swarm.hives";
names = hiveNames;