refactor(nix): a hive's domain comes out of the swarm directory
`services.hyperhive.domain` and `swarm.hives.<hiveName>.domain` were two
homes for one value with nothing asserting they agreed. The failure that
buys is the worst shape a config defect has: it evaluates cleanly, and
the symptom ("the other hives can't reach me") appears on a machine
other than the misconfigured one.
The directory is now the single source. `hives.<name>.domain` gains the
`<name>.<swarm.domain>` default -- a derivation from two values an
operator had to state explicitly, not a guess -- so a conventional swarm
is a list of names and a hive addressed by something else says so in the
one place every other hive reads. `services.hyperhive.domain` reads its
own entry; the direct formula is deleted rather than kept as a fallback,
which would have restored the second path (and, reading `swarm.domain`
itself, a second path that can disagree).
Setting it directly still wins, with a deprecation warning: nothing
breaks today, but a value written only there is invisible to the swarm.
The self-entry assertion now fires on an EMPTY directory too. Since
`swarm.domain` became required, every hive is in a swarm -- a swarm of
one is still a swarm -- and this host's address is read out of the
directory, so the entry is missing either way and the precise message
should be the one that fires.
Upgrading costs one line on hives that never listed themselves:
`services.hyperhive.swarm.hives.<hiveName> = { };`, no value.
This commit is contained in:
parent
30551464c1
commit
3b6576faee
5 changed files with 221 additions and 122 deletions
|
|
@ -44,103 +44,121 @@ in
|
|||
{
|
||||
options.services.hyperhive.swarm.hives = lib.mkOption {
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
domain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "lab.example.com";
|
||||
description = ''
|
||||
Public DNS domain this hive occupies — used for dashboard
|
||||
links, peer HTTPS checks and Matrix federation discovery.
|
||||
lib.types.submodule (
|
||||
{ name, ... }:
|
||||
{
|
||||
options = {
|
||||
domain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
# `<name>.<swarm.domain>` is a derivation from two values an
|
||||
# operator had to state explicitly (both are required), not a
|
||||
# guess — and it is what makes this directory worth copying:
|
||||
# a conventional swarm is `{ pr1ma = { }; umbra = { }; }`,
|
||||
# names only, with a non-conventional hive saying so and only
|
||||
# that. A shared file is read far more often than written.
|
||||
#
|
||||
# Total rather than a throw when `swarm.domain` is unset, for
|
||||
# the reason ./hive-network.nix:155 gives in full: defaults
|
||||
# that interpolate the domain are forced *while the assertion
|
||||
# list evaluates*, so a throw here would replace the message
|
||||
# naming the missing option with a coercion error naming this
|
||||
# one. `.invalid` is reserved (RFC 2606) and fails loudly at
|
||||
# resolution if it ever escaped — which the required-domain
|
||||
# assertion is there to stop.
|
||||
default = if swarmCfg.domain == null then "${name}.invalid" else "${name}.${swarmCfg.domain}";
|
||||
defaultText = lib.literalExpression ''"''${name}.''${services.hyperhive.swarm.domain}"'';
|
||||
example = "lab.example.com";
|
||||
description = ''
|
||||
Public DNS domain this hive occupies — used for dashboard
|
||||
links, peer HTTPS checks and Matrix federation discovery.
|
||||
|
||||
Deliberately has no default. It is conventionally
|
||||
`<name>.<swarm.domain>`, but defaulting to that would let
|
||||
a typo'd swarm domain produce a name that resolves
|
||||
somewhere real; an eval failure naming the hive is the
|
||||
better outcome.
|
||||
'';
|
||||
Defaults to `<name>.<swarm.domain>`, the convention every
|
||||
hive in a swarm follows, so a conventional directory is
|
||||
names only. Set it for a hive that is addressed by
|
||||
something else.
|
||||
'';
|
||||
};
|
||||
|
||||
certFingerprint = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "sha256:b1946ac92492d2347c6235b4d2611184a3f5b6cae6c19d6e3c2f0a8e7d4c9f12";
|
||||
description = ''
|
||||
Expected TLS certificate fingerprint for this hive's HTTPS
|
||||
endpoint. Null = trust the CA bundle — which for a hive
|
||||
inside the swarm CA hierarchy is the normal case, since
|
||||
every hive under the swarm root already chains to it.
|
||||
Set it to pin a leaf that no CA in the bundle vouches for.
|
||||
|
||||
Format: the literal `sha256:` followed by exactly 64
|
||||
hex digits (case-insensitive, no colon separators) — the
|
||||
SHA-256 digest of the DER-encoded leaf certificate.
|
||||
Generate with `openssl x509 -noout -fingerprint -sha256`,
|
||||
then strip the colons and prepend `sha256:`. A malformed
|
||||
value is ignored with a warning rather than weakening
|
||||
trust. See docs/swarm/README.md for the full recipe.
|
||||
|
||||
Scopes only to hive-c0re's own peer HTTPS checks — it does
|
||||
NOT help Matrix federation, which validates against the
|
||||
container's trust bundle. There is no per-hive CA field to
|
||||
cover that case any more: the swarm root is the trust path
|
||||
(see ./swarm-ca.nix).
|
||||
'';
|
||||
};
|
||||
|
||||
wireguardPublicKey = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "base64pubkey=";
|
||||
description = ''
|
||||
WireGuard public key for this hive's host. Required when
|
||||
`services.hyperhive.swarm.wireguard.enable = true` and
|
||||
you want this hive reachable over the mesh. Null = TLS-
|
||||
only peering (public internet, no mesh tunnel).
|
||||
'';
|
||||
};
|
||||
|
||||
wireguardEndpoint = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "203.0.113.1:51820";
|
||||
description = ''
|
||||
WireGuard endpoint for this hive in `host:port` form.
|
||||
Null = this hive has no reachable endpoint, so the tunnel
|
||||
is initiated from the other side.
|
||||
|
||||
Reads like a fact about the relationship and is not: it
|
||||
says whether *this* hive can be dialled, which every other
|
||||
hive in the swarm needs the same answer to.
|
||||
'';
|
||||
};
|
||||
|
||||
wireguardAddress = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "10.100.0.2/32";
|
||||
description = ''
|
||||
IP address (with prefix) of this hive's host on the
|
||||
WireGuard mesh. Used as the `allowedIPs` for its
|
||||
WireGuard config entry and injected into `HYPERHIVE_PEERS`
|
||||
so hive-c0re can route intra-swarm traffic to the mesh
|
||||
address rather than the public domain. Required to include
|
||||
a hive in the mesh (entries missing this field are
|
||||
silently excluded from `wg-hive`).
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
certFingerprint = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "sha256:b1946ac92492d2347c6235b4d2611184a3f5b6cae6c19d6e3c2f0a8e7d4c9f12";
|
||||
description = ''
|
||||
Expected TLS certificate fingerprint for this hive's HTTPS
|
||||
endpoint. Null = trust the CA bundle — which for a hive
|
||||
inside the swarm CA hierarchy is the normal case, since
|
||||
every hive under the swarm root already chains to it.
|
||||
Set it to pin a leaf that no CA in the bundle vouches for.
|
||||
|
||||
Format: the literal `sha256:` followed by exactly 64
|
||||
hex digits (case-insensitive, no colon separators) — the
|
||||
SHA-256 digest of the DER-encoded leaf certificate.
|
||||
Generate with `openssl x509 -noout -fingerprint -sha256`,
|
||||
then strip the colons and prepend `sha256:`. A malformed
|
||||
value is ignored with a warning rather than weakening
|
||||
trust. See docs/swarm/README.md for the full recipe.
|
||||
|
||||
Scopes only to hive-c0re's own peer HTTPS checks — it does
|
||||
NOT help Matrix federation, which validates against the
|
||||
container's trust bundle. There is no per-hive CA field to
|
||||
cover that case any more: the swarm root is the trust path
|
||||
(see ./swarm-ca.nix).
|
||||
'';
|
||||
};
|
||||
|
||||
wireguardPublicKey = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "base64pubkey=";
|
||||
description = ''
|
||||
WireGuard public key for this hive's host. Required when
|
||||
`services.hyperhive.swarm.wireguard.enable = true` and
|
||||
you want this hive reachable over the mesh. Null = TLS-
|
||||
only peering (public internet, no mesh tunnel).
|
||||
'';
|
||||
};
|
||||
|
||||
wireguardEndpoint = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "203.0.113.1:51820";
|
||||
description = ''
|
||||
WireGuard endpoint for this hive in `host:port` form.
|
||||
Null = this hive has no reachable endpoint, so the tunnel
|
||||
is initiated from the other side.
|
||||
|
||||
Reads like a fact about the relationship and is not: it
|
||||
says whether *this* hive can be dialled, which every other
|
||||
hive in the swarm needs the same answer to.
|
||||
'';
|
||||
};
|
||||
|
||||
wireguardAddress = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "10.100.0.2/32";
|
||||
description = ''
|
||||
IP address (with prefix) of this hive's host on the
|
||||
WireGuard mesh. Used as the `allowedIPs` for its
|
||||
WireGuard config entry and injected into `HYPERHIVE_PEERS`
|
||||
so hive-c0re can route intra-swarm traffic to the mesh
|
||||
address rather than the public domain. Required to include
|
||||
a hive in the mesh (entries missing this field are
|
||||
silently excluded from `wg-hive`).
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
default = { };
|
||||
example = {
|
||||
pr1ma = {
|
||||
domain = "pr1ma.example.com";
|
||||
wireguardAddress = "10.100.0.1/32";
|
||||
wireguardEndpoint = "203.0.113.1:51820";
|
||||
};
|
||||
edge = {
|
||||
domain = "edge.example.com";
|
||||
domain = "edge.elsewhere.example";
|
||||
wireguardAddress = "10.100.0.2/32";
|
||||
};
|
||||
};
|
||||
|
|
@ -151,10 +169,15 @@ in
|
|||
`services.hyperhive.hiveName` is what makes a given host read it
|
||||
as "me and four others" rather than "five peers".
|
||||
|
||||
Empty (the default) means this host is not part of a swarm. Once
|
||||
non-empty it must contain an entry for `hiveName`, which is
|
||||
asserted — a hive that lists everyone but itself would otherwise
|
||||
derive its own peer set as *everything* and peer with itself.
|
||||
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
|
||||
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
|
||||
*everything* and peer with itself.
|
||||
'';
|
||||
};
|
||||
|
||||
|
|
@ -197,20 +220,33 @@ in
|
|||
|
||||
assertions = [
|
||||
{
|
||||
# An EMPTY `hives` fires this too, deliberately: since
|
||||
# `swarm.domain` became 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 no longer 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 = swarmCfg.hives == { } || cfg.hiveName == null || swarmCfg.hives ? ${cfg.hiveName};
|
||||
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. Add:
|
||||
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}" = {
|
||||
domain = "${toString cfg.domain}";
|
||||
};
|
||||
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)}
|
||||
'';
|
||||
|
|
|
|||
Loading…
Reference in a new issue