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
|
|
@ -131,7 +131,7 @@ covers spreading a swarm across hosts.
|
||||||
deploy.hive-controller.enable = true; # …and a hive to run agents on
|
deploy.hive-controller.enable = true; # …and a hive to run agents on
|
||||||
|
|
||||||
swarm.domain = "hive.example"; # the swarm's DNS domain
|
swarm.domain = "hive.example"; # the swarm's DNS domain
|
||||||
hiveName = "pr1ma"; # this hive's label in it
|
deploy.hive-controller.hiveName = "pr1ma"; # this hive's label in it
|
||||||
swarm.hives.pr1ma = { }; # the directory: just us
|
swarm.hives.pr1ma = { }; # the directory: just us
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ You rarely switch it on yourself. `gateway.enable` defaults to off, and every mo
|
||||||
|
|
||||||
A service module declares its vhost as `services.hyperhive.gateway.vhosts.<name>`: the locations, `forceSSL` and any `extraConfig` it needs. The gateway renders it into `services.nginx.virtualHosts.<name>` and adds the `listen` set, the certificate for that name and the [security headers](#security-headers) (ahead of the vhost's own `extraConfig`), and drops `addSSL` from a vhost that sets `forceSSL`. The eval fails for a vhost that sets `listen`, `addSSL`, `onlySSL`, `enableACME`, `useACMEHost`, `sslCertificate` or `sslCertificateKey` itself, and for any vhost declared while `gateway.enable` is false. A name with a certificate from outside the gateway's leaves (the queue's) sets `certificate = { cert; key; }` instead. `hive-gateway/vhosts.nix` declares the gateway's own catch-all (`_`) the same way, `hive-c0re/vhosts.nix` the dashboard and the hive domain, and `glue-matrix-hive-discovery.nix` adds the matrix redirect to the hive domain where the homeserver runs, and discovery there when the hive domain is the pinned server name. The catch-all sets `securityHeaders = false` because it sends no response, and the gateway renders a vhost's `default` at `mkDefault` priority.
|
A service module declares its vhost as `services.hyperhive.gateway.vhosts.<name>`: the locations, `forceSSL` and any `extraConfig` it needs. The gateway renders it into `services.nginx.virtualHosts.<name>` and adds the `listen` set, the certificate for that name and the [security headers](#security-headers) (ahead of the vhost's own `extraConfig`), and drops `addSSL` from a vhost that sets `forceSSL`. The eval fails for a vhost that sets `listen`, `addSSL`, `onlySSL`, `enableACME`, `useACMEHost`, `sslCertificate` or `sslCertificateKey` itself, and for any vhost declared while `gateway.enable` is false. A name with a certificate from outside the gateway's leaves (the queue's) sets `certificate = { cert; key; }` instead. `hive-gateway/vhosts.nix` declares the gateway's own catch-all (`_`) the same way, `hive-c0re/vhosts.nix` the dashboard and the hive domain, and `glue-matrix-hive-discovery.nix` adds the matrix redirect to the hive domain where the homeserver runs, and discovery there when the hive domain is the pinned server name. The catch-all sets `securityHeaders = false` because it sends no response, and the gateway renders a vhost's `default` at `mkDefault` priority.
|
||||||
|
|
||||||
**The hive's own vhosts**: the dashboard's, named `<name>.hives.<swarm>` (`swarm.dashboardDomain`), and one named for the hive domain. They exist only on a host that runs a hive (`deploy.hive-controller.enable`); a host without a hive has none of these rows:
|
**The hive's own vhosts**: the dashboard's, named `<name>.hives.<swarm>` (`deploy.hive-controller.dashboardDomain`), and one named for the hive domain. They exist only on a host that runs a hive (`deploy.hive-controller.enable`); a host without a hive has none of these rows:
|
||||||
|
|
||||||
| URL | upstream | when |
|
| URL | upstream | when |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
|
@ -116,7 +116,7 @@ Give the gateway the real cert (`tls.certDir` or `tls.acme`) so it serves proper
|
||||||
|
|
||||||
## Dashboard access
|
## Dashboard access
|
||||||
|
|
||||||
The dashboard answers on `<name>.hives.<swarm>` (`swarm.dashboardDomain`, derived from the hive name and `swarm.domain`), behind an authelia `auth_request`. Log in with your authelia account; only accounts in the `admins` group get in, the same rule as the swarm UI. Add one with `swarmctl user add <name> --group admins` ([`swarm/sso.md`](../swarm/sso.md)).
|
The dashboard answers on `<name>.hives.<swarm>` (`deploy.hive-controller.dashboardDomain`, derived from the hive name and `swarm.domain`), behind an authelia `auth_request`. Log in with your authelia account; only accounts in the `admins` group get in, the same rule as the swarm UI. Add one with `swarmctl user add <name> --group admins` ([`swarm/sso.md`](../swarm/sso.md)).
|
||||||
|
|
||||||
The name sits under the swarm domain so authelia's session cookie reaches it, whatever the hive's own domain is. Any path on the hive domain other than the matrix routes redirects there, so old bookmarks still land.
|
The name sits under the swarm domain so authelia's session cookie reaches it, whatever the hive's own domain is. Any path on the hive domain other than the matrix routes redirects there, so old bookmarks still land.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ listener sits on `bridgeIp`, on the host's bridge interface.
|
||||||
{
|
{
|
||||||
services.hyperhive = {
|
services.hyperhive = {
|
||||||
deploy.hive-controller.enable = true;
|
deploy.hive-controller.enable = true;
|
||||||
hiveName = "pr1ma";
|
deploy.hive-controller.hiveName = "pr1ma";
|
||||||
swarm.domain = "darkest.space";
|
swarm.domain = "darkest.space";
|
||||||
swarm.hives.pr1ma = { }; # -> domain = pr1ma.darkest.space
|
swarm.hives.pr1ma = { }; # -> domain = pr1ma.darkest.space
|
||||||
# network.bridgeIp = "10.42.0.1"; # default
|
# network.bridgeIp = "10.42.0.1"; # default
|
||||||
|
|
|
||||||
|
|
@ -232,7 +232,7 @@ status_text, status_set_at, hive_name, swarm_name, matrix_accounts }`:
|
||||||
or when `running = false` (see above).
|
or when `running = false` (see above).
|
||||||
- `hive_name` / `swarm_name`: display names read from
|
- `hive_name` / `swarm_name`: display names read from
|
||||||
`HYPERHIVE_HIVE_NAME` / `HYPERHIVE_SWARM_NAME` env (sourced from
|
`HYPERHIVE_HIVE_NAME` / `HYPERHIVE_SWARM_NAME` env (sourced from
|
||||||
`services.hyperhive.hiveName` / `services.hyperhive.swarm.name`).
|
`services.hyperhive.deploy.hive-controller.hiveName` / `services.hyperhive.swarm.name`).
|
||||||
Both `None` when the options aren't configured.
|
Both `None` when the options aren't configured.
|
||||||
- `matrix_accounts`: one `MatrixIdentity` per configured + live matrix
|
- `matrix_accounts`: one `MatrixIdentity` per configured + live matrix
|
||||||
account the agent can act as. Empty for agents with no matrix
|
account the agent can act as. Empty for agents with no matrix
|
||||||
|
|
|
||||||
|
|
@ -485,7 +485,7 @@ refused delivery logs `webhook: refused delivery` with the reason.
|
||||||
```nix
|
```nix
|
||||||
services.hyperhive = {
|
services.hyperhive = {
|
||||||
swarm.domain = "example.com"; # required — the swarm's DNS domain
|
swarm.domain = "example.com"; # required — the swarm's DNS domain
|
||||||
hiveName = "pr1ma"; # required — this hive's label in it
|
deploy.hive-controller.hiveName = "pr1ma"; # required on a hive host — this hive's label in it
|
||||||
swarm.name = "constellat1on"; # shared swarm display name (optional)
|
swarm.name = "constellat1on"; # shared swarm display name (optional)
|
||||||
|
|
||||||
# required — the directory, identical on every host in the swarm.
|
# required — the directory, identical on every host in the swarm.
|
||||||
|
|
@ -503,7 +503,7 @@ services.hyperhive = {
|
||||||
Swarm options are identical on every host in the swarm, so `swarm.domain` is
|
Swarm options are identical on every host in the swarm, so `swarm.domain` is
|
||||||
**required** on every host that runs any hyperhive service; a host that
|
**required** on every host that runs any hyperhive service; a host that
|
||||||
imports the module and enables nothing evaluates without it. `hiveName` is
|
imports the module and enables nothing evaluates without it. `hiveName` is
|
||||||
required on a host that runs a hive, the secret store or the homeserver. Eval fails with a hint naming
|
required on a host that runs a hive (`deploy.hive-controller.hiveName`), and on no other. Eval fails with a hint naming
|
||||||
each. Neither defaults, because a guessed value here is a wrong hostname
|
each. Neither defaults, because a guessed value here is a wrong hostname
|
||||||
that evaluates cleanly and deploys.
|
that evaluates cleanly and deploys.
|
||||||
|
|
||||||
|
|
@ -518,7 +518,7 @@ directory without this host would make every hive a peer, itself included.
|
||||||
|
|
||||||
```
|
```
|
||||||
# hive A # hive B
|
# hive A # hive B
|
||||||
hiveName = "pr1ma"; hiveName = "edge";
|
deploy.hive-controller.hiveName = "pr1ma"; deploy.hive-controller.hiveName = "edge";
|
||||||
swarm.hives = { … }; swarm.hives = { … }; # byte-identical
|
swarm.hives = { … }; swarm.hives = { … }; # byte-identical
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -313,7 +313,7 @@ impl AgentServer {
|
||||||
and the target's self-reported `status` text (set via `set_status`) plus how \
|
and the target's self-reported `status` text (set via `set_status`) plus how \
|
||||||
long ago it was set. Also returns the hive + swarm display names (`hive_name`, \
|
long ago it was set. Also returns the hive + swarm display names (`hive_name`, \
|
||||||
`swarm_name`) when the operator has configured \
|
`swarm_name`) when the operator has configured \
|
||||||
`services.hyperhive.hiveName` / `services.hyperhive.swarm.name`; both lines \
|
`services.hyperhive.deploy.hive-controller.hiveName` / `services.hyperhive.swarm.name`; both lines \
|
||||||
omitted when unset. Pass `name` to query a peer (e.g. \
|
omitted when unset. Pass `name` to query a peer (e.g. \
|
||||||
check whether iris is idle before pinging them); omit `name` to get your own \
|
check whether iris is idle before pinging them); omit `name` to get your own \
|
||||||
identity stamp — handy for state files / commit messages / cross-agent \
|
identity stamp — handy for state files / commit messages / cross-agent \
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ pub fn hive_domain() -> Option<String> {
|
||||||
/// [`hive_domain`] — the domain is the machine-readable DNS address;
|
/// [`hive_domain`] — the domain is the machine-readable DNS address;
|
||||||
/// this is the prose label humans use in conversation. Reads the
|
/// this is the prose label humans use in conversation. Reads the
|
||||||
/// `HYPERHIVE_HIVE_NAME` env var; returns None when it is unset or empty
|
/// `HYPERHIVE_HIVE_NAME` env var; returns None when it is unset or empty
|
||||||
/// (the host's `services.hyperhive.hiveName` is unset), in which case
|
/// (the host's `services.hyperhive.deploy.hive-controller.hiveName` is unset), in which case
|
||||||
/// callers fall back to the domain or the short label at their discretion.
|
/// callers fall back to the domain or the short label at their discretion.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn hive_name() -> Option<String> {
|
pub fn hive_name() -> Option<String> {
|
||||||
|
|
|
||||||
|
|
@ -361,7 +361,7 @@ fn read_active_model(name: &hive_types::Ident) -> Option<String> {
|
||||||
|
|
||||||
/// Host-side hive + swarm display names, read from the c0re service's
|
/// Host-side hive + swarm display names, read from the c0re service's
|
||||||
/// own process env. The hyperhive NixOS module sets these from
|
/// own process env. The hyperhive NixOS module sets these from
|
||||||
/// `services.hyperhive.hiveName` + `services.hyperhive.swarm.name`
|
/// `services.hyperhive.deploy.hive-controller.hiveName` + `services.hyperhive.swarm.name`
|
||||||
/// (the hive names itself; the swarm it joins is named one level out).
|
/// (the hive names itself; the swarm it joins is named one level out).
|
||||||
/// The agent-side
|
/// The agent-side
|
||||||
/// `hive-agent::identity::{hive_name, swarm_name}` accessors read the
|
/// `hive-agent::identity::{hive_name, swarm_name}` accessors read the
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ pub(super) struct StateSnapshot {
|
||||||
authelia_public_url: Option<String>,
|
authelia_public_url: Option<String>,
|
||||||
/// Human name of this single-host hive instance (e.g. `"pr1ma"`).
|
/// Human name of this single-host hive instance (e.g. `"pr1ma"`).
|
||||||
/// Sourced from `HYPERHIVE_HIVE_NAME` env var, set by the c0re
|
/// Sourced from `HYPERHIVE_HIVE_NAME` env var, set by the c0re
|
||||||
/// NixOS module from `services.hyperhive.hiveName`. `None` when
|
/// NixOS module from `services.hyperhive.deploy.hive-controller.hiveName`. `None` when
|
||||||
/// the option is unset — chrome falls back to `hostname`.
|
/// the option is unset — chrome falls back to `hostname`.
|
||||||
hive_name: Option<String>,
|
hive_name: Option<String>,
|
||||||
/// Human name of the wider swarm this hive belongs to (e.g.
|
/// Human name of the wider swarm this hive belongs to (e.g.
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@
|
||||||
//! same host, each internally consistent.
|
//! same host, each internally consistent.
|
||||||
//!
|
//!
|
||||||
//! **The key is this hive's `hiveName`.** Nothing here has to arrange for
|
//! **The key is this hive's `hiveName`.** Nothing here has to arrange for
|
||||||
//! that to match the controller's roster: `swarm.nix` asserts that
|
//! that to match the controller's roster: `hyperhive.nix` asserts that
|
||||||
//! `services.hyperhive.hiveName` is a key of `services.hyperhive.swarm.hives`,
|
//! `services.hyperhive.deploy.hive-controller.hiveName` is a key of `services.hyperhive.swarm.hives`,
|
||||||
//! so a hive that evaluates at all publishes under a name the roster
|
//! so a hive that evaluates at all publishes under a name the roster
|
||||||
//! knows. The alternative — a key the controller has never heard of —
|
//! knows. The alternative — a key the controller has never heard of —
|
||||||
//! renders as `unknown` rather than being silently dropped, but the
|
//! renders as `unknown` rather than being silently dropped, but the
|
||||||
|
|
|
||||||
|
|
@ -387,7 +387,7 @@ pub struct HiveUrls {
|
||||||
/// Canonical hive domain (`services.hyperhive.domain`).
|
/// Canonical hive domain (`services.hyperhive.domain`).
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub domain: Option<String>,
|
pub domain: Option<String>,
|
||||||
/// Operator dashboard root (`https://<services.hyperhive.swarm.dashboardDomain>/`).
|
/// Operator dashboard root (`https://<services.hyperhive.deploy.hive-controller.dashboardDomain>/`).
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub home: Option<String>,
|
pub home: Option<String>,
|
||||||
/// Forge browser URL (`HIVE_FORGE_PUBLIC_URL`) — `None` when
|
/// Forge browser URL (`HIVE_FORGE_PUBLIC_URL`) — `None` when
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,9 @@ const HIVE_TLS_TRUST_BUNDLE_PATH: &str = "/var/lib/hive-tls/trust-bundle.pem";
|
||||||
/// *and future* hive validates.
|
/// *and future* hive validates.
|
||||||
const STORE_PKI_ROOT_PATH: &str = "/var/lib/hive-tls/swarm-services-root.pem";
|
const STORE_PKI_ROOT_PATH: &str = "/var/lib/hive-tls/swarm-services-root.pem";
|
||||||
|
|
||||||
|
/// The option whose value keys a hive's entry in `services.hyperhive.swarm.hives`.
|
||||||
|
const HIVE_NAME_OPTION: &str = "services.hyperhive.deploy.hive-controller.hiveName";
|
||||||
|
|
||||||
/// Attrset key for a hive in `services.hyperhive.swarm.hives`, derived
|
/// Attrset key for a hive in `services.hyperhive.swarm.hives`, derived
|
||||||
/// from its domain's first DNS label.
|
/// from its domain's first DNS label.
|
||||||
///
|
///
|
||||||
|
|
@ -177,6 +180,16 @@ fn wg_pubkey(privkey: &[u8]) -> Result<String> {
|
||||||
Ok(String::from_utf8_lossy(&out.stdout).trim().to_owned())
|
Ok(String::from_utf8_lossy(&out.stdout).trim().to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The closing comment of the `wg peer` snippet.
|
||||||
|
fn wg_peer_key_note() -> String {
|
||||||
|
format!(" # the key must be that hive's {HIVE_NAME_OPTION}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The first closing comment line of the `peer-config` snippet.
|
||||||
|
fn peer_config_key_note() -> String {
|
||||||
|
format!("# the key must be this hive's {HIVE_NAME_OPTION}, and the")
|
||||||
|
}
|
||||||
|
|
||||||
/// `wg peer` — print the nix snippet to add a peer hive to the mesh.
|
/// `wg peer` — print the nix snippet to add a peer hive to the mesh.
|
||||||
/// Pure output (no fallible work), so it returns `()`; the dispatch arm
|
/// Pure output (no fallible work), so it returns `()`; the dispatch arm
|
||||||
/// wraps it in `Ok` to match the sibling verbs.
|
/// wraps it in `Ok` to match the sibling verbs.
|
||||||
|
|
@ -193,7 +206,7 @@ pub(crate) fn wg_peer(domain: &str, pubkey: &str, address: &str, endpoint: Optio
|
||||||
println!(" wireguardEndpoint = \"{ep}\";");
|
println!(" wireguardEndpoint = \"{ep}\";");
|
||||||
}
|
}
|
||||||
println!(" }};");
|
println!(" }};");
|
||||||
println!(" # the key must be that hive's services.hyperhive.hiveName");
|
println!("{}", wg_peer_key_note());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The `peer-config` step carrying the secret store's `pki` root: the
|
/// The `peer-config` step carrying the secret store's `pki` root: the
|
||||||
|
|
@ -248,7 +261,7 @@ pub(crate) fn peer_config(domain: &str, wg_address: Option<&str>, wg_endpoint: O
|
||||||
println!(" wireguardEndpoint = \"{ep}\";");
|
println!(" wireguardEndpoint = \"{ep}\";");
|
||||||
}
|
}
|
||||||
println!("}};");
|
println!("}};");
|
||||||
println!("# the key must be this hive's services.hyperhive.hiveName, and the");
|
println!("{}", peer_config_key_note());
|
||||||
println!("# same entry belongs in every hive's config — `hives` is the swarm.");
|
println!("# same entry belongs in every hive's config — `hives` is the swarm.");
|
||||||
if !self_signed {
|
if !self_signed {
|
||||||
println!(
|
println!(
|
||||||
|
|
@ -276,10 +289,26 @@ pub(crate) fn wg_status() -> Result<()> {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{STORE_PKI_ROOT_PATH, store_root_step, write_key_0400};
|
use super::{
|
||||||
|
STORE_PKI_ROOT_PATH, peer_config_key_note, store_root_step, wg_peer_key_note,
|
||||||
|
write_key_0400,
|
||||||
|
};
|
||||||
|
|
||||||
const PEM: &str = "-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----";
|
const PEM: &str = "-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----";
|
||||||
|
|
||||||
|
/// Both snippets tell the operator to set the deploy option, never the
|
||||||
|
/// removed `services.hyperhive` spelling.
|
||||||
|
#[test]
|
||||||
|
fn key_notes_name_the_deploy_option() {
|
||||||
|
for note in [wg_peer_key_note(), peer_config_key_note()] {
|
||||||
|
assert!(
|
||||||
|
note.contains("services.hyperhive.deploy.hive-controller.hiveName"),
|
||||||
|
"{note}"
|
||||||
|
);
|
||||||
|
assert!(!note.contains("services.hyperhive.hiveName"), "{note}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The certificate is printed whole, on lines of its own, so a peer
|
/// The certificate is printed whole, on lines of its own, so a peer
|
||||||
/// operator can copy it out of the output as a PEM file.
|
/// operator can copy it out of the output as a PEM file.
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,12 @@ in
|
||||||
#
|
#
|
||||||
# The runner moved from `deploy.forgejo.ci` to `deploy.ci-runner` with
|
# The runner moved from `deploy.forgejo.ci` to `deploy.ci-runner` with
|
||||||
# every option keeping its type and meaning, so the old paths forward.
|
# 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
|
(lib.mkRenamedOptionModule
|
||||||
[ "services" "hyperhive" "deploy" "forgejo" "ci" "enable" ]
|
[ "services" "hyperhive" "deploy" "forgejo" "ci" "enable" ]
|
||||||
[ "services" "hyperhive" "deploy" "ci-runner" "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 {
|
swarm-controller.enable = lib.mkOption {
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
default = false;
|
default = false;
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ let
|
||||||
|
|
||||||
hiveOtelSecret = import ./lib/hive-otel-secret.nix {
|
hiveOtelSecret = import ./lib/hive-otel-secret.nix {
|
||||||
inherit (hyperhiveCfg.swarm.authelia) hiveClientPrefix;
|
inherit (hyperhiveCfg.swarm.authelia) hiveClientPrefix;
|
||||||
inherit (hyperhiveCfg) hiveName;
|
inherit (hyperhiveCfg.deploy.hive-controller) hiveName;
|
||||||
};
|
};
|
||||||
|
|
||||||
havePair = cert: key: cert != null && key != null;
|
havePair = cert: key: cert != null && key != null;
|
||||||
|
|
@ -47,7 +47,7 @@ let
|
||||||
# under the hive's own leaf.
|
# under the hive's own leaf.
|
||||||
swarm-bao-hive-otel-oidc =
|
swarm-bao-hive-otel-oidc =
|
||||||
otel.collector.enable
|
otel.collector.enable
|
||||||
&& hyperhiveCfg.hiveName != null
|
&& hyperhiveCfg.deploy.hive-controller.hiveName != null
|
||||||
&& havePair baoDeploy.clientCertFile baoDeploy.clientKeyFile
|
&& havePair baoDeploy.clientCertFile baoDeploy.clientKeyFile
|
||||||
&& otel.clientSecretFile == hiveOtelSecret.path;
|
&& otel.clientSecretFile == hiveOtelSecret.path;
|
||||||
# ./swarm-grafana.nix
|
# ./swarm-grafana.nix
|
||||||
|
|
|
||||||
|
|
@ -41,16 +41,18 @@ let
|
||||||
# re-issuing every client certificate in the swarm.
|
# re-issuing every client certificate in the swarm.
|
||||||
pkiDir = import ./lib/bao-pki-dir.nix;
|
pkiDir = import ./lib/bao-pki-dir.nix;
|
||||||
|
|
||||||
# What a reader calls itself to the store. The hive's name, because a bao
|
# The hive this host runs, if any, and what it calls itself to the store: the
|
||||||
# cert-auth role matches on the CN — this is an interface, not a label.
|
# hive's name, because a bao cert-auth role matches on the CN — this is an
|
||||||
# No fallback: `hiveName` is asserted set wherever `deploy.bao.enable` is
|
# interface, not a label. `hiveName` is asserted set wherever
|
||||||
# (./hyperhive.nix), the same condition this file's `config` is gated on. A
|
# `deploy.hive-controller.enable` is (./hyperhive.nix), and the reader leaf
|
||||||
# fallback here reads as a second supported spelling and there is no such
|
# below exists only there: a store host that runs no hive has no hive to
|
||||||
# thing.
|
# name, and a fallback CN would read as a second supported spelling.
|
||||||
clientCn = hyperhiveCfg.hiveName;
|
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
|
# Every other hive in the swarm directory. This host's own hive, where there
|
||||||
# into the top of the directory, beside the leaves its units read.
|
# 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);
|
otherHives = lib.filter (h: h != clientCn) (lib.attrNames hyperhiveCfg.swarm.hives);
|
||||||
|
|
||||||
# $1 dir $2 basename $3 CN $4 SAN or "" $5 EKU
|
# $1 dir $2 basename $3 CN $4 SAN or "" $5 EKU
|
||||||
|
|
@ -83,15 +85,16 @@ in
|
||||||
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";
|
||||||
clientCaFile = lib.mkDefault "${pkiDir}/ca.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";
|
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
|
# 🩸 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
|
# `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
|
# 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.
|
# CA is a file rather than a service.
|
||||||
[ -s ${pkiDir}/server.pem ] || ${signLeaf} ${pkiDir} server \
|
[ -s ${pkiDir}/server.pem ] || ${signLeaf} ${pkiDir} server \
|
||||||
${lib.escapeShellArg cfg.domain} ${lib.escapeShellArg "DNS:${cfg.domain}"} serverAuth
|
${lib.escapeShellArg cfg.domain} ${lib.escapeShellArg "DNS:${cfg.domain}"} serverAuth
|
||||||
|
${lib.optionalString runsHive ''
|
||||||
[ -s ${pkiDir}/client.pem ] || ${signLeaf} ${pkiDir} client \
|
[ -s ${pkiDir}/client.pem ] || ${signLeaf} ${pkiDir} client \
|
||||||
${lib.escapeShellArg clientCn} "" clientAuth
|
${lib.escapeShellArg clientCn} "" clientAuth
|
||||||
|
''}
|
||||||
|
|
||||||
# Minted whether or not a controller runs here, because the case it
|
# 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
|
# 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 \
|
[ -s ${pkiDir}/forge-publisher.pem ] || ${signLeaf} ${pkiDir} forge-publisher \
|
||||||
${lib.escapeShellArg deployCfg.bao.forgePublisherCommonName} "" clientAuth
|
${lib.escapeShellArg deployCfg.bao.forgePublisherCommonName} "" clientAuth
|
||||||
|
|
||||||
${lib.optionalString deployCfg.hive-controller.enable ''
|
${lib.optionalString runsHive ''
|
||||||
# This hive's gateway-leaf identity, which opens
|
# This hive's gateway-leaf identity, which opens
|
||||||
# `pki/issue/<hiveLeafPkiRolePrefix>-<hive>` and nothing else. Its
|
# `pki/issue/<hiveLeafPkiRolePrefix>-<hive>` and nothing else. Its
|
||||||
# subject carries this hive's name, because ./swarm-bao.nix writes
|
# 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.
|
# 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.
|
# 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
|
# ⚠️ Gated on a PKI directory existing (the store runs here, or the operator
|
||||||
# on the store's own host is one deployment; a controller three networks away
|
# named a reader leaf), not on the store alone. A controller on the store's own
|
||||||
# holding a leaf issued out of band is another, and both want the same wiring.
|
# host is one deployment; a controller three networks away holding a leaf
|
||||||
# `deploy.bao.enable` would have made the first one the only supported shape.
|
# issued out of band is another, and both want the same wiring.
|
||||||
#
|
#
|
||||||
# Everything is `mkDefault`. An operator naming their own paths wins.
|
# Everything is `mkDefault`. An operator naming their own paths wins.
|
||||||
{
|
{
|
||||||
|
|
@ -25,11 +25,18 @@ let
|
||||||
deployCfg = hyperhiveCfg.deploy;
|
deployCfg = hyperhiveCfg.deploy;
|
||||||
baoDeploy = deployCfg.bao;
|
baoDeploy = deployCfg.bao;
|
||||||
|
|
||||||
# Where ./glue-bao-tls.nix puts the leaves. Derived from the reader's own
|
# Where ./glue-bao-tls.nix puts the leaves. An operator's own `clientCertFile`
|
||||||
# path rather than repeating that file's directory literal: an operator who
|
# names the directory when one is set (an operator who moves the PKI moves
|
||||||
# moves the PKI moves both, and the two cannot drift apart.
|
# both, and the two cannot drift apart); on a store host that runs no hive
|
||||||
haveMintedPki = baoDeploy.clientCertFile != null;
|
# there is no reader leaf to derive from, so it is the store's directory.
|
||||||
pkiDir = if haveMintedPki then builtins.dirOf baoDeploy.clientCertFile else null;
|
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
|
in
|
||||||
{
|
{
|
||||||
config = lib.mkIf (deployCfg.swarm-controller.enable && haveMintedPki) {
|
config = lib.mkIf (deployCfg.swarm-controller.enable && haveMintedPki) {
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,13 @@ let
|
||||||
autheliaMachine = hyperhiveCfg.swarm.authelia.machine;
|
autheliaMachine = hyperhiveCfg.swarm.authelia.machine;
|
||||||
|
|
||||||
# ./otel.nix: `senderAuth`, `hiveLeaf`, `fetches`, `fetchedSecretPath`.
|
# ./otel.nix: `senderAuth`, `hiveLeaf`, `fetches`, `fetchedSecretPath`.
|
||||||
senderAuth = otel.clientSecretFile != null && hyperhiveCfg.hiveName != null;
|
senderAuth = otel.clientSecretFile != null && hyperhiveCfg.deploy.hive-controller.hiveName != null;
|
||||||
hiveLeaf =
|
hiveLeaf =
|
||||||
hyperhiveCfg.deploy.bao.clientCertFile != null && hyperhiveCfg.deploy.bao.clientKeyFile != null;
|
hyperhiveCfg.deploy.bao.clientCertFile != null && hyperhiveCfg.deploy.bao.clientKeyFile != null;
|
||||||
fetchedSecretPath =
|
fetchedSecretPath =
|
||||||
(import ./lib/hive-otel-secret.nix {
|
(import ./lib/hive-otel-secret.nix {
|
||||||
inherit (hyperhiveCfg.swarm.authelia) hiveClientPrefix;
|
inherit (hyperhiveCfg.swarm.authelia) hiveClientPrefix;
|
||||||
inherit (hyperhiveCfg) hiveName;
|
inherit (hyperhiveCfg.deploy.hive-controller) hiveName;
|
||||||
}).path;
|
}).path;
|
||||||
fetches = senderAuth && hiveLeaf && otel.clientSecretFile == fetchedSecretPath;
|
fetches = senderAuth && hiveLeaf && otel.clientSecretFile == fetchedSecretPath;
|
||||||
waits = senderAuth && !fetches;
|
waits = senderAuth && !fetches;
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ let
|
||||||
matrixCfg.gatewayHost != null
|
matrixCfg.gatewayHost != null
|
||||||
&& hyperhiveCfg.deploy.hive-controller.enable
|
&& hyperhiveCfg.deploy.hive-controller.enable
|
||||||
&& gatewayCfg.enable
|
&& gatewayCfg.enable
|
||||||
&& hyperhiveCfg.swarm.dashboardDomain != null;
|
&& hyperhiveCfg.deploy.hive-controller.dashboardDomain != null;
|
||||||
|
|
||||||
# Same default as `effectiveServerName` in ./hive-matrix.nix.
|
# Same default as `effectiveServerName` in ./hive-matrix.nix.
|
||||||
serverName =
|
serverName =
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
# belongs here is the pairing: which host paths go to the publisher in
|
# belongs here is the pairing: which host paths go to the publisher in
|
||||||
# authelia's container.
|
# 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
|
# ./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
|
# host is one deployment; a publisher beside a remote authelia holding a leaf
|
||||||
# issued out of band is another, and both want this wiring.
|
# issued out of band is another, and both want this wiring.
|
||||||
|
|
@ -25,11 +25,18 @@ let
|
||||||
deployCfg = hyperhiveCfg.deploy;
|
deployCfg = hyperhiveCfg.deploy;
|
||||||
baoDeploy = deployCfg.bao;
|
baoDeploy = deployCfg.bao;
|
||||||
|
|
||||||
# Where ./glue-bao-tls.nix puts the leaves, derived from the reader's own
|
# Where ./glue-bao-tls.nix puts the leaves. An operator's own `clientCertFile`
|
||||||
# path rather than repeating that file's directory literal: an operator who
|
# names the directory when one is set (an operator who moves the PKI moves
|
||||||
# moves the PKI moves both, and the two cannot drift apart.
|
# both, and the two cannot drift apart); on a store host that runs no hive
|
||||||
haveMintedPki = baoDeploy.clientCertFile != null;
|
# there is no reader leaf to derive from, so it is the store's directory.
|
||||||
pkiDir = if haveMintedPki then builtins.dirOf baoDeploy.clientCertFile else null;
|
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
|
in
|
||||||
{
|
{
|
||||||
config = lib.mkIf (deployCfg.swarm-secret-publisher.enable && haveMintedPki) {
|
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.
|
# required (asserted in hive-network.nix), so it's always set.
|
||||||
HYPERHIVE_HIVE_DOMAIN = config.services.hyperhive.domain;
|
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
|
# The host the dashboard and the agent UIs answer on, for the URLs c0re
|
||||||
# hands out (`hivectl open home`, each agent's UI link).
|
# 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) {
|
// lib.optionalAttrs (config.services.hyperhive.deploy.hive-controller.hiveName != null) {
|
||||||
HYPERHIVE_HIVE_NAME = config.services.hyperhive.hiveName;
|
HYPERHIVE_HIVE_NAME = config.services.hyperhive.deploy.hive-controller.hiveName;
|
||||||
}
|
}
|
||||||
// lib.optionalAttrs (config.services.hyperhive.swarm.name != null) {
|
// lib.optionalAttrs (config.services.hyperhive.swarm.name != null) {
|
||||||
HYPERHIVE_SWARM_NAME = config.services.hyperhive.swarm.name;
|
HYPERHIVE_SWARM_NAME = config.services.hyperhive.swarm.name;
|
||||||
|
|
@ -221,7 +221,7 @@ in
|
||||||
# The identity swarm-authelia.nix already declares for every entry 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
|
# `swarm.hives` — the hive does not choose its own name here, it uses
|
||||||
# the one the roster gave it.
|
# 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
|
# 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`
|
# 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;
|
# 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
|
# 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
|
# dashboard's name. The matrix redirect, and discovery for a server name pinned to
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
let
|
let
|
||||||
hyperhiveCfg = config.services.hyperhive;
|
hyperhiveCfg = config.services.hyperhive;
|
||||||
gatewayCfg = hyperhiveCfg.gateway;
|
gatewayCfg = hyperhiveCfg.gateway;
|
||||||
inherit (hyperhiveCfg.swarm) dashboardDomain;
|
inherit (hyperhiveCfg.deploy.hive-controller) dashboardDomain;
|
||||||
autheliaDomain = hyperhiveCfg.swarm.authelia.domain;
|
autheliaDomain = hyperhiveCfg.swarm.authelia.domain;
|
||||||
inherit (gatewayCfg.lib) errorPages;
|
inherit (gatewayCfg.lib) errorPages;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -706,10 +706,8 @@ in
|
||||||
# per-host flag is false exactly where this entry is needed.
|
# per-host flag is false exactly where this entry is needed.
|
||||||
webhook.ALLOWED_HOST_LIST = lib.concatStringsSep "," (
|
webhook.ALLOWED_HOST_LIST = lib.concatStringsSep "," (
|
||||||
lib.unique (
|
lib.unique (
|
||||||
[
|
[ "external" ]
|
||||||
"external"
|
++ lib.optional (hyperhiveDomain != null) hyperhiveDomain
|
||||||
hyperhiveDomain
|
|
||||||
]
|
|
||||||
++ lib.optional (swarmDomain != null) swarmDomain
|
++ lib.optional (swarmDomain != null) swarmDomain
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -423,7 +423,7 @@ in
|
||||||
# answer; a service added later lands here with no edit, and cannot
|
# answer; a service added later lands here with no edit, and cannot
|
||||||
# land here with a different condition than it used for DNS.
|
# land here with a different condition than it used for DNS.
|
||||||
networking.hosts = lib.mkIf cfg.localHostsEntry {
|
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,9 +52,8 @@
|
||||||
# Hive authoritative records — answer queries for the hive domain
|
# Hive authoritative records — answer queries for the hive domain
|
||||||
# + its sub-domains with the bridge IP, where nginx is reachable
|
# + its sub-domains with the bridge IP, where nginx is reachable
|
||||||
# from every container netns.
|
# from every container netns.
|
||||||
address = [
|
address =
|
||||||
"/${hyperhiveDomain}/${networkCfg.bridgeIp}"
|
lib.optional (hyperhiveDomain != null) "/${hyperhiveDomain}/${networkCfg.bridgeIp}"
|
||||||
]
|
|
||||||
# Names contributed by the modules that own them
|
# Names contributed by the modules that own them
|
||||||
# (`gateway.localNames`). Same address as everything above — the
|
# (`gateway.localNames`). Same address as everything above — the
|
||||||
# bridge IP is the gateway's answer for anything it fronts, and a
|
# bridge IP is the gateway's answer for anything it fronts, and a
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
(lib.mkRemovedOptionModule [ "services" "hyperhive" "gateway" "auth" ] ''
|
(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
|
behind authelia, and only accounts in authelia's `admins` group can
|
||||||
open it. There is no HTTP Basic auth and no htpasswd file. Remove
|
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`.
|
the setting; add an operator with `swarmctl user add <name> --group admins`.
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ let
|
||||||
networkCfg = config.services.hyperhive.network;
|
networkCfg = config.services.hyperhive.network;
|
||||||
tlsCfg = config.services.hyperhive.deploy.hive-controller.tls;
|
tlsCfg = config.services.hyperhive.deploy.hive-controller.tls;
|
||||||
gatewayCfg = config.services.hyperhive.gateway;
|
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;
|
hyperhiveDomain = config.services.hyperhive.domain;
|
||||||
|
|
||||||
# Same runtime→build-time bridge hive-forge and swarm-ci-runner cross:
|
# 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
|
# FluffyChat boot-config pre-fill so the client's
|
||||||
# `.well-known/matrix/client` lookup hits the right delegation
|
# `.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" = {
|
"= /config.json" = {
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
default_type application/json;
|
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
|
which each hive occupies one sub-domain. A guessed value would
|
||||||
be a wrong hostname that evaluates cleanly and deploys. Set it
|
be a wrong hostname that evaluates cleanly and deploys. Set it
|
||||||
(`services.hyperhive.swarm.domain = "example.com";`) — with
|
(`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
|
# goes on the swarm-services leaf below, and asking this role for one is a
|
||||||
# refusal.
|
# refusal.
|
||||||
hiveLeafRole =
|
hiveLeafRole =
|
||||||
if hyperhiveCfg.hiveName == null then
|
if hyperhiveCfg.deploy.hive-controller.hiveName == null then
|
||||||
null
|
null
|
||||||
else
|
else
|
||||||
"${baoDeploy.hiveLeafPkiRolePrefix}-${hyperhiveCfg.hiveName}";
|
"${baoDeploy.hiveLeafPkiRolePrefix}-${hyperhiveCfg.deploy.hive-controller.hiveName}";
|
||||||
|
|
||||||
# Everything after the first rides as a SAN.
|
# Everything after the first rides as a SAN.
|
||||||
hiveLeafNames = [
|
hiveLeafNames =
|
||||||
|
lib.optionals (domain != null) [
|
||||||
domain
|
domain
|
||||||
"*.${domain}"
|
"*.${domain}"
|
||||||
]
|
]
|
||||||
++ lib.optional (hyperhiveCfg.swarm.dashboardDomain != null) hyperhiveCfg.swarm.dashboardDomain;
|
++ lib.optional (
|
||||||
|
hyperhiveCfg.deploy.hive-controller.dashboardDomain != null
|
||||||
|
) hyperhiveCfg.deploy.hive-controller.dashboardDomain;
|
||||||
|
|
||||||
showPath = p: if p == null then "unset" else p;
|
showPath = p: if p == null then "unset" else p;
|
||||||
|
|
||||||
|
|
@ -522,7 +525,7 @@ in
|
||||||
# even while its current leaf is still valid.
|
# even while its current leaf is still valid.
|
||||||
${
|
${
|
||||||
if hiveLeafRole == null then
|
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
|
else
|
||||||
requireHiveIdentity "ask the store for its gateway leaf"
|
requireHiveIdentity "ask the store for its gateway leaf"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
hiveCfg = config.services.hyperhive;
|
hiveCfg = config.services.hyperhive;
|
||||||
|
hiveName = hiveCfg.deploy.hive-controller.hiveName;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
|
|
@ -46,8 +47,8 @@ in
|
||||||
# would recreate the second path (and, since that default reads
|
# would recreate the second path (and, since that default reads
|
||||||
# `swarm.domain` too, a value that can differ from the directory's).
|
# `swarm.domain` too, a value that can differ from the directory's).
|
||||||
default =
|
default =
|
||||||
if hiveCfg.hiveName != null && hiveCfg.swarm.hives ? ${hiveCfg.hiveName} then
|
if hiveName != null && hiveCfg.swarm.hives ? ${hiveName} then
|
||||||
hiveCfg.swarm.hives.${hiveCfg.hiveName}.domain
|
hiveCfg.swarm.hives.${hiveName}.domain
|
||||||
else
|
else
|
||||||
null;
|
null;
|
||||||
defaultText = lib.literalExpression "services.hyperhive.swarm.hives.\${hiveName}.domain, or null when there is no entry for this hive";
|
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
|
hive's address belongs in the swarm directory, which every hive in
|
||||||
the swarm shares a copy of:
|
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
|
(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
|
||||||
|
|
@ -115,7 +116,7 @@ in
|
||||||
occupies its own sub-domain of it, which is why every entry in
|
occupies its own sub-domain of it, which is why every entry in
|
||||||
`services.hyperhive.swarm.hives` defaults its `domain` to
|
`services.hyperhive.swarm.hives` defaults its `domain` to
|
||||||
`<name>.<swarm.domain>` — set this plus
|
`<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.
|
hive in the swarm states an address at all.
|
||||||
|
|
||||||
**Required** on every host that runs a hyperhive service (a
|
**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
|
# The hive's name is `deploy.hive-controller.hiveName`, declared in
|
||||||
# above (machine-readable) — see docs/process/conventions.md::Hive identity
|
# ./deploy.nix beside the switch that makes a host run a hive. The
|
||||||
# for the domain-vs-name-vs-swarm distinction + the env-var
|
# identity surfaces (label, domain, display names, env-var chain) are in
|
||||||
# propagation chain. The swarm's display name is
|
# docs/process/conventions.md::Hive identity.
|
||||||
# `services.hyperhive.swarm.name`, one level out: this hive is named
|
#
|
||||||
# here, the swarm it belongs to is named there.
|
# A hive turns the name into an identifier with no fallback: hive-c0re's
|
||||||
options.services.hyperhive.hiveName = lib.mkOption {
|
# OIDC client and matrix localpart, the CN of the hive's `client.pem`
|
||||||
type = lib.types.nullOr lib.types.str;
|
# (./glue-bao-tls.nix), and the `hives/<name>` path of its secrets. A null
|
||||||
default = null;
|
# there renders as an empty string that evaluates and deploys.
|
||||||
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.
|
|
||||||
config.assertions = [
|
config.assertions = [
|
||||||
{
|
{
|
||||||
assertion =
|
assertion = !hiveCfg.deploy.hive-controller.enable || hiveName != null;
|
||||||
!(
|
|
||||||
hiveCfg.deploy.hive-controller.enable || hiveCfg.deploy.bao.enable || hiveCfg.deploy.matrix.enable
|
|
||||||
)
|
|
||||||
|| hiveCfg.hiveName != null;
|
|
||||||
message = ''
|
message = ''
|
||||||
hyperhive requires services.hyperhive.hiveName to be set on a host
|
hyperhive requires services.hyperhive.deploy.hive-controller.hiveName
|
||||||
that runs a hive, the swarm's secret store or its homeserver — it is
|
to be set on a host that runs a hive — it is this hive's label
|
||||||
this hive's label within the swarm, and the leftmost part of the
|
within the swarm, and the leftmost part of the domain it is
|
||||||
domain it is addressed by (`<hiveName>.<swarm.domain>`), not only a
|
addressed by (`<hiveName>.<swarm.domain>`), not only a display name.
|
||||||
display name. Set it (`services.hyperhive.hiveName = "pr1ma";`).
|
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,
|
# 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 the hive's name.
|
||||||
options.services.hyperhive.swarm.name = lib.mkOption {
|
options.services.hyperhive.swarm.name = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
default = null;
|
default = null;
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ let
|
||||||
# Where `swarm-bao-hive-otel-oidc` lands the secret on this host.
|
# Where `swarm-bao-hive-otel-oidc` lands the secret on this host.
|
||||||
hiveOtelSecret = import ./lib/hive-otel-secret.nix {
|
hiveOtelSecret = import ./lib/hive-otel-secret.nix {
|
||||||
inherit (config.services.hyperhive.swarm.authelia) hiveClientPrefix;
|
inherit (config.services.hyperhive.swarm.authelia) hiveClientPrefix;
|
||||||
inherit (config.services.hyperhive) hiveName;
|
inherit (config.services.hyperhive.deploy.hive-controller) hiveName;
|
||||||
};
|
};
|
||||||
fetchedSecretPath = hiveOtelSecret.path;
|
fetchedSecretPath = hiveOtelSecret.path;
|
||||||
in
|
in
|
||||||
|
|
@ -190,7 +190,10 @@ in
|
||||||
clientSecretFile = lib.mkOption {
|
clientSecretFile = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
default =
|
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'';
|
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";
|
example = "/var/lib/secrets/hive-telemetry.secret";
|
||||||
description = ''
|
description = ''
|
||||||
|
|
@ -256,7 +259,7 @@ in
|
||||||
deployCfg = config.services.hyperhive.deploy;
|
deployCfg = config.services.hyperhive.deploy;
|
||||||
swarmOtelCfg = config.services.hyperhive.swarm.otel;
|
swarmOtelCfg = config.services.hyperhive.swarm.otel;
|
||||||
hiveOtelCfg = config.services.hyperhive.swarm.hiveOtel;
|
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}";
|
listen = "${config.services.hyperhive.network.bridgeIp}:${toString hiveOtelCfg.collectorPort}";
|
||||||
swarmName = "otlphttp/swarm";
|
swarmName = "otlphttp/swarm";
|
||||||
gatewayCfg = config.services.hyperhive.gateway;
|
gatewayCfg = config.services.hyperhive.gateway;
|
||||||
|
|
@ -884,7 +887,9 @@ in
|
||||||
services.hyperhive.otel.clientSecretFile = ${
|
services.hyperhive.otel.clientSecretFile = ${
|
||||||
if otel.clientSecretFile == null then "null" else 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
|
Every hive authenticates as itself — that is what makes the
|
||||||
`hive` label on its metrics mean anything — so both are
|
`hive` label on its metrics mean anything — so both are
|
||||||
|
|
|
||||||
|
|
@ -1310,9 +1310,10 @@ let
|
||||||
# same class of surprise it exists to remove.
|
# same class of surprise it exists to remove.
|
||||||
baoCli =
|
baoCli =
|
||||||
let
|
let
|
||||||
# The reader's identity `glue-bao-tls` already mints on this host, so
|
# The hive reader identity `glue-bao-tls` mints where a hive runs, so
|
||||||
# cert-auth login needs nothing typed either. A token stays the
|
# cert-auth login needs nothing typed either. A store host without a hive
|
||||||
# operator's to supply — a wrapper cannot conjure one.
|
# has no such identity and the operator supplies a token or a leaf; a
|
||||||
|
# wrapper cannot conjure either.
|
||||||
wrapArgs = [
|
wrapArgs = [
|
||||||
"--set-default BAO_ADDR ${lib.escapeShellArg "https://${cfg.domain}:${toString cfg.port}"}"
|
"--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,
|
# 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
|
# 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.
|
# operator's view across hives.
|
||||||
#
|
#
|
||||||
# Options, and the swarm apex's vhost, declared below through
|
# Options, and the swarm apex's vhost, declared below through
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@
|
||||||
# our own entry would configure a tunnel to ourselves.
|
# our own entry would configure a tunnel to ourselves.
|
||||||
meshPeers = lib.filterAttrs (
|
meshPeers = lib.filterAttrs (
|
||||||
name: p:
|
name: p:
|
||||||
name != config.services.hyperhive.hiveName
|
name != config.services.hyperhive.deploy.hive-controller.hiveName
|
||||||
&& p.wireguardPublicKey != null
|
&& p.wireguardPublicKey != null
|
||||||
&& p.wireguardAddress != null
|
&& p.wireguardAddress != null
|
||||||
) config.services.hyperhive.swarm.hives;
|
) config.services.hyperhive.swarm.hives;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# The swarm's directory: one entry per hive, **including this one**,
|
# The swarm's directory: one entry per hive, **including this one**,
|
||||||
# identical on every host in the swarm. `services.hyperhive.hiveName`
|
# identical on every host in the swarm. A host that runs a hive says which
|
||||||
# says which entry is us.
|
# 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
|
# 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
|
# 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
|
Every hive in this swarm, keyed by `hiveName` — **including this
|
||||||
host's own hive**. The same attrset is meant to be identical on
|
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;
|
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".
|
as "me and four others" rather than "five peers".
|
||||||
|
|
||||||
Each entry needs no fields at all in the conventional case: a
|
Each entry needs no fields at all in the conventional case: a
|
||||||
hive's `domain` defaults to `<name>.<swarm.domain>`, so the whole
|
hive's `domain` defaults to `<name>.<swarm.domain>`, so the whole
|
||||||
directory is usually a list of names.
|
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
|
host's own address is read out of it (it is where
|
||||||
`services.hyperhive.domain` derives from), and a hive that lists
|
`services.hyperhive.domain` derives from), and a hive that lists
|
||||||
everyone but itself would otherwise derive its own peer set as
|
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 {
|
options.services.hyperhive.swarm.serviceDomains = lib.mkOption {
|
||||||
type = lib.types.listOf lib.types.str;
|
type = lib.types.listOf lib.types.str;
|
||||||
readOnly = true;
|
readOnly = true;
|
||||||
|
|
@ -284,49 +274,11 @@ in
|
||||||
if swarmCfg.domain == null then "${name}.hives.invalid" else "${name}.hives.${swarmCfg.domain}"
|
if swarmCfg.domain == null then "${name}.hives.invalid" else "${name}.hives.${swarmCfg.domain}"
|
||||||
) swarmCfg.hives;
|
) 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) (
|
services.hyperhive.swarm.serviceDomains = lib.sort (a: b: a < b) (
|
||||||
lib.unique (lib.filter (d: d != null && d != "") serviceDomains')
|
lib.unique (lib.filter (d: d != null && d != "") serviceDomains')
|
||||||
);
|
);
|
||||||
|
|
||||||
assertions = [
|
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 {
|
(nameGuards.mustBeIdent {
|
||||||
option = "services.hyperhive.swarm.hives";
|
option = "services.hyperhive.swarm.hives";
|
||||||
names = hiveNames;
|
names = hiveNames;
|
||||||
|
|
|
||||||
|
|
@ -568,7 +568,7 @@ let
|
||||||
l = vh."h1.hives.t.local".locations;
|
l = vh."h1.hives.t.local".locations;
|
||||||
gated = loc: lib.hasInfix "auth_request /__hive_authelia;" loc.extraConfig;
|
gated = loc: lib.hasInfix "auth_request /__hive_authelia;" loc.extraConfig;
|
||||||
in
|
in
|
||||||
allLocal.services.hyperhive.swarm.dashboardDomain == "h1.hives.t.local"
|
allLocal.services.hyperhive.deploy.hive-controller.dashboardDomain == "h1.hives.t.local"
|
||||||
&& vh."h1.hives.t.local".forceSSL
|
&& vh."h1.hives.t.local".forceSSL
|
||||||
&& lib.all (p: gated l.${p}) [
|
&& lib.all (p: gated l.${p}) [
|
||||||
"/"
|
"/"
|
||||||
|
|
@ -1251,23 +1251,133 @@ let
|
||||||
&& bare.networking.bridges ? hive-br0;
|
&& bare.networking.bridges ? hive-br0;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# Refused wherever something turns the name into an identifier, and only
|
# Refused where a hive runs and nowhere else. A name is an identifier
|
||||||
# there. The store host is the control that the refusal is not the hive's
|
# only for a hive; the hive host is the control that the lookup finds the
|
||||||
# alone; the host running neither is the control that it is gated at all.
|
# refusal when there is one.
|
||||||
name = "a missing hiveName is refused on a hive and on a store host, not elsewhere";
|
name = "a missing hiveName is refused on a hive and not on a store, homeserver or services host";
|
||||||
ok =
|
ok =
|
||||||
let
|
let
|
||||||
refusedHiveName =
|
refused =
|
||||||
extra:
|
extra:
|
||||||
lib.any (a: !a.assertion && lib.hasInfix "services.hyperhive.hiveName to be set" a.message)
|
lib.any (
|
||||||
(hiveWithController ({ hiveName = null; } // extra)).assertions;
|
a:
|
||||||
|
!a.assertion
|
||||||
|
&& lib.hasInfix "services.hyperhive.deploy.hive-controller.hiveName" a.message
|
||||||
|
&& lib.hasInfix "to be set" a.message
|
||||||
|
) (hive (lib.recursiveUpdate { deploy.hive-controller.hiveName = null; } extra)).assertions;
|
||||||
in
|
in
|
||||||
refusedHiveName { }
|
refused { deploy.hive-controller.enable = true; }
|
||||||
&& refusedHiveName {
|
&& !(refused { deploy.bao.enable = true; })
|
||||||
deploy.hive-controller.enable = false;
|
&& !(refused { deploy.matrix.enable = true; })
|
||||||
deploy.bao.enable = true;
|
&& !(refused { deploy.allSwarmServices = true; });
|
||||||
}
|
}
|
||||||
&& !(refusedHiveName { deploy.hive-controller.enable = false; });
|
{
|
||||||
|
# The directory entry is asserted for a hive and only for a hive. The
|
||||||
|
# hive host with a name the directory lacks is the control.
|
||||||
|
name = "a hive whose name has no swarm.hives entry is refused, and a host without a hive is not asked";
|
||||||
|
ok =
|
||||||
|
let
|
||||||
|
noEntry =
|
||||||
|
extra:
|
||||||
|
lib.any (a: !a.assertion && lib.hasInfix "swarm.hives has no entry for this hive" a.message)
|
||||||
|
(hive (lib.recursiveUpdate { deploy.hive-controller.hiveName = "other"; } extra)).assertions;
|
||||||
|
in
|
||||||
|
noEntry { deploy.hive-controller.enable = true; }
|
||||||
|
&& !(noEntry { deploy.bao.enable = true; })
|
||||||
|
&& !(noEntry { deploy.allSwarmServices = true; });
|
||||||
|
}
|
||||||
|
{
|
||||||
|
# Nothing of the hive's identity is minted for a host that runs no hive:
|
||||||
|
# no reader leaf named after a hive, and no reader identity at all, so
|
||||||
|
# the glue that locates the PKI falls back to the store's own directory.
|
||||||
|
# The hive host with the same store is the control.
|
||||||
|
name = "a store host without a hive mints no hive reader leaf and still wires the controller and publisher";
|
||||||
|
ok =
|
||||||
|
let
|
||||||
|
storeNoHive = hive {
|
||||||
|
deploy.hive-controller.hiveName = null;
|
||||||
|
deploy.bao.enable = true;
|
||||||
|
deploy.swarm-controller.enable = true;
|
||||||
|
deploy.swarm-secret-publisher.enable = true;
|
||||||
|
};
|
||||||
|
storeWithHive = hiveWithController {
|
||||||
|
deploy.bao.enable = true;
|
||||||
|
deploy.swarm-controller.enable = true;
|
||||||
|
deploy.swarm-secret-publisher.enable = true;
|
||||||
|
};
|
||||||
|
pki = m: m.systemd.services.swarm-bao-pki.script;
|
||||||
|
d = m: m.services.hyperhive.deploy;
|
||||||
|
in
|
||||||
|
(d storeNoHive).bao.clientCertFile == null
|
||||||
|
&& (d storeNoHive).bao.clientKeyFile == null
|
||||||
|
&& !(lib.hasInfix "/var/lib/swarm-bao-pki/client.pem ]" (pki storeNoHive))
|
||||||
|
&& (d storeNoHive).swarm-controller.baoClientCertFile == "/var/lib/swarm-bao-pki/controller.pem"
|
||||||
|
&&
|
||||||
|
(d storeNoHive).swarm-secret-publisher.baoClientCertFile
|
||||||
|
== "/var/lib/swarm-bao-pki/secret-publisher.pem"
|
||||||
|
&& (d storeWithHive).bao.clientCertFile == "/var/lib/swarm-bao-pki/client.pem"
|
||||||
|
&& lib.hasInfix "/var/lib/swarm-bao-pki/client.pem ]" (pki storeWithHive)
|
||||||
|
&& (d storeWithHive).swarm-controller.baoClientCertFile == "/var/lib/swarm-bao-pki/controller.pem";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
# An operator's own reader leaf names the PKI directory, exactly as it did
|
||||||
|
# when the glue always derived it from that path.
|
||||||
|
name = "a controller and publisher with an operator-placed reader leaf and no store follow its directory";
|
||||||
|
ok =
|
||||||
|
let
|
||||||
|
m = hive {
|
||||||
|
deploy.swarm-controller.enable = true;
|
||||||
|
deploy.swarm-secret-publisher.enable = true;
|
||||||
|
deploy.bao.clientCertFile = "/etc/pki/elsewhere/client.pem";
|
||||||
|
deploy.bao.clientKeyFile = "/etc/pki/elsewhere/client-key.pem";
|
||||||
|
};
|
||||||
|
d = m.services.hyperhive.deploy;
|
||||||
|
in
|
||||||
|
!d.bao.enable
|
||||||
|
&& d.swarm-controller.baoClientCertFile == "/etc/pki/elsewhere/controller.pem"
|
||||||
|
&& d.swarm-secret-publisher.baoClientCertFile == "/etc/pki/elsewhere/secret-publisher.pem";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
# The old spelling forwards with the module system's one warning and
|
||||||
|
# produces the same hive. The new-path host is the control that the
|
||||||
|
# warning is not always present.
|
||||||
|
name = "services.hyperhive.hiveName still sets the hive's name, with a rename warning";
|
||||||
|
ok =
|
||||||
|
let
|
||||||
|
old = hiveWithController {
|
||||||
|
hiveName = "legacy";
|
||||||
|
swarm.hives.legacy = { };
|
||||||
|
};
|
||||||
|
warned =
|
||||||
|
m:
|
||||||
|
lib.any (
|
||||||
|
w: lib.hasInfix "services.hyperhive.hiveName" w && lib.hasInfix "has been renamed" w
|
||||||
|
) m.warnings;
|
||||||
|
in
|
||||||
|
old.services.hyperhive.deploy.hive-controller.hiveName == "legacy"
|
||||||
|
&& old.systemd.services.hive-c0re.environment.HYPERHIVE_HIVE_NAME == "legacy"
|
||||||
|
&& warned old
|
||||||
|
&& !(warned bare);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
# A hive's rendered identity, pinned so moving the option changes none of
|
||||||
|
# it: the coordinator's name and client, the dashboard name, the CN of the
|
||||||
|
# reader leaf the store host signs, and what the hive collector presents.
|
||||||
|
name = "a hive's name reaches hive-c0re, the reader leaf, the dashboard and the hive collector unchanged";
|
||||||
|
ok =
|
||||||
|
let
|
||||||
|
e = allLocal.systemd.services.hive-c0re.environment;
|
||||||
|
script = allLocal.systemd.services.swarm-bao-pki.script;
|
||||||
|
lines = lib.splitString "\n" script;
|
||||||
|
at = lib.lists.findFirstIndex (lib.hasSuffix "swarm-bao-pki client \\") null lines;
|
||||||
|
nextLine = builtins.head (builtins.match " *(.*)" (builtins.elemAt lines (at + 1)));
|
||||||
|
in
|
||||||
|
e.HYPERHIVE_HIVE_NAME == "h1"
|
||||||
|
&& e.HIVE_C0RE_OIDC_CLIENT_ID == "hive-h1"
|
||||||
|
&& e.HYPERHIVE_DASHBOARD_DOMAIN == "h1.hives.t.local"
|
||||||
|
&& allLocal.services.hyperhive.deploy.hive-controller.dashboardDomain == "h1.hives.t.local"
|
||||||
|
&& at != null
|
||||||
|
&& nextLine == ''h1 "" clientAuth'';
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# The control is a host that defines no such option; the removal message
|
# The control is a host that defines no such option; the removal message
|
||||||
|
|
|
||||||
|
|
@ -391,6 +391,19 @@ let
|
||||||
&& !(builtins.elem container (wait baoWithHiveOtel).after)
|
&& !(builtins.elem container (wait baoWithHiveOtel).after)
|
||||||
&& !(builtins.elem container (wait baoWithHiveOtel).requires);
|
&& !(builtins.elem container (wait baoWithHiveOtel).requires);
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
# What the hive collector presents and where it presents it, both derived
|
||||||
|
# from the hive's name: pinned so the name's option path can move without
|
||||||
|
# the collector's identity changing.
|
||||||
|
name = "the hive collector authenticates as hive-<name> and pushes to the swarm collector under the name's path";
|
||||||
|
ok =
|
||||||
|
let
|
||||||
|
otel = hiveOtel.services.hyperhive.swarm.otel;
|
||||||
|
in
|
||||||
|
hiveOtelSettings.extensions."oauth2client/swarm".client_id == "hive-h1"
|
||||||
|
&& hiveOtelSettings.extensions."oauth2client/swarm".endpoint_params.audience == "hive-h1"
|
||||||
|
&& hiveOtelSettings.exporters."otlphttp/swarm".endpoint == "https://${otel.domain}/h1";
|
||||||
|
}
|
||||||
];
|
];
|
||||||
in
|
in
|
||||||
runGroup "hive-otel" cases
|
runGroup "hive-otel" cases
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,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 {
|
||||||
hiveName = "h1";
|
deploy.hive-controller.hiveName = lib.mkDefault "h1";
|
||||||
swarm.domain = "t.local";
|
swarm.domain = "t.local";
|
||||||
swarm.hives.h1.domain = "h1.t.local";
|
swarm.hives.h1.domain = "h1.t.local";
|
||||||
} extra;
|
} extra;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue