feat(#1897): auto-fill the hive domain in peer-config (drop required --domain)

Per operator review: hivectl shouldn't make the operator retype this
hive's own domain. Add a HostRequest::HiveDomain admin-socket query
(c0re answers from HYPERHIVE_HIVE_DOMAIN, already in its service env) and
a domain field on HostResponse.

hivectl peer-config now resolves the domain as: --domain override (kept
for offline/scripted use), else the daemon query; errors with a clear
hint when neither resolves. wg init resolves it best-effort too, so it
prints the hand-over peer-config block without --domain (skipped, not
failed, when the daemon is unreachable).

Regenerated docs/tools/hivectl-cli.md.
This commit is contained in:
atlas 2026-06-22 18:49:09 +02:00 committed by mara
commit 72d9422a7a
4 changed files with 89 additions and 19 deletions

View file

@ -94,14 +94,16 @@ enum Cmd {
/// to trust + reach this hive. Emits `caCert` (+ a `cp` line for the
/// cert) when this hive serves a self-signed CA, the WireGuard public
/// key when the mesh key exists, and the `wireguard{Address,Endpoint}`
/// you pass. Pure output — reads local state (the TLS CA cert, the wg
/// key), never mutates. `wg init` calls this at the end when given a
/// `--domain`, so a fresh mesh setup prints the hand-over block too.
/// you pass. Reads local state (the TLS CA cert, the wg key) + asks the
/// daemon for this hive's domain; never mutates. `wg init` calls this
/// at the end, so a fresh mesh setup prints the hand-over block too.
PeerConfig {
/// This hive's DNS domain — the `swarm.peers` attrset key the peer
/// declares. Required: hivectl has no other source for it.
/// Override this hive's DNS domain (the `swarm.peers` attrset key
/// the peer declares). Omit to auto-fill from the running daemon
/// (`services.hyperhive.domain`); pass it only when the daemon is
/// down or you're scripting offline.
#[arg(long)]
domain: String,
domain: Option<String>,
/// This hive's WireGuard mesh address (e.g. `10.42.0.1/32`),
/// emitted as `wireguardAddress`. Omit when not running the mesh.
#[arg(long)]
@ -590,7 +592,9 @@ async fn main() -> Result<()> {
AgentsCmd::RestartAll => agents_restart_all(&socket).await,
},
Cmd::Wg { cmd } => match cmd {
WgCmd::Init { address, domain } => wg_init(address.as_deref(), domain.as_deref()),
WgCmd::Init { address, domain } => {
wg_init(&socket, address.as_deref(), domain.as_deref()).await
}
WgCmd::Peer {
domain,
pubkey,
@ -607,6 +611,7 @@ async fn main() -> Result<()> {
wg_address,
wg_endpoint,
} => {
let domain = resolve_hive_domain(&socket, domain).await?;
peer_config(&domain, wg_address.as_deref(), wg_endpoint.as_deref());
Ok(())
}
@ -655,11 +660,38 @@ const WG_INTERFACE: &str = "wg-hive";
/// = ACME / operator cert (trusted by the default CA bundle, no `caCert`).
const HIVE_TLS_CA_PATH: &str = "/var/lib/hive-tls/ca.pem";
/// Best-effort query for this hive's domain from the running daemon
/// (`HostRequest::HiveDomain`, which reads `HYPERHIVE_HIVE_DOMAIN` from
/// c0re's service env). `None` when the daemon is unreachable or the
/// domain is unset — callers decide whether that's fatal.
async fn query_hive_domain(socket: &Path) -> Option<String> {
hive_c0re::client::request(socket, hive_sh4re::HostRequest::HiveDomain)
.await
.ok()
.and_then(|r| r.domain)
}
/// Resolve this hive's domain for snippet generation: the explicit
/// `--domain` override if given, else the daemon. Errors with a clear
/// hint when neither is available, so `peer-config` never silently emits
/// a wrong key.
async fn resolve_hive_domain(socket: &Path, over: Option<String>) -> Result<String> {
if let Some(d) = over {
return Ok(d);
}
query_hive_domain(socket).await.context(
"could not determine this hive's domain from the daemon — is hive-c0re running \
and `services.hyperhive.domain` set? pass --domain to override",
)
}
/// `wg init` — generate (if absent) the hive's WireGuard key, print its
/// public key + the nix snippet to enable the mesh. When `domain` is set,
/// also prints the `peer-config` block peers paste to federate with this
/// hive (so a fresh setup is one command).
fn wg_init(address: Option<&str>, domain: Option<&str>) -> Result<()> {
/// public key + the nix snippet to enable the mesh, then (best-effort)
/// the `peer-config` block peers paste to federate with this hive, so a
/// fresh setup is one command. The domain comes from `--domain` or the
/// daemon; if neither resolves, the peer block is skipped (init still
/// succeeds — its core job is enabling the mesh locally).
async fn wg_init(socket: &Path, address: Option<&str>, domain: Option<&str>) -> Result<()> {
use std::os::unix::fs::PermissionsExt as _;
let key_path = Path::new(WG_KEY_PATH);
if key_path.exists() {
@ -703,11 +735,16 @@ fn wg_init(address: Option<&str>, domain: Option<&str>) -> Result<()> {
println!(" # listenPort = 51820; # default");
println!(" }};");
// When the operator names this hive's domain, also print the block a
// peer pastes to federate with us (CA + this mesh key) — one-stop setup.
if let Some(d) = domain {
// Also print the block a peer pastes to federate with us (CA + this
// mesh key) — one-stop setup. Domain from --domain or the daemon;
// best-effort, so init still succeeds when neither resolves.
let resolved = match domain {
Some(d) => Some(d.to_owned()),
None => query_hive_domain(socket).await,
};
if let Some(d) = resolved {
println!();
peer_config(d, address, None);
peer_config(&d, address, None);
}
Ok(())
}