move hiveName to deploy.hive-controller; only a hive host needs a name
This commit is contained in:
parent
67f0d6a9d3
commit
0c6e8fc5a2
37 changed files with 402 additions and 215 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}"}';
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}"}"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue