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:
parent
8464cb95cb
commit
72d9422a7a
4 changed files with 89 additions and 19 deletions
|
|
@ -50,7 +50,7 @@ Sibling to the `hive-c0re` daemon binary. Covers host-side admin operations that
|
|||
* `gateway` — Gateway htpasswd user management. Add, remove, or list users in an htpasswd file used by the gateway's HTTP Basic auth (`services.hyperhive.gateway.auth`). Credentials are stored as `BCrypt` hashes — no extra service or PAM required
|
||||
* `agents` — Agent container management. Requires the hive-c0re daemon to be running (connects to the host admin socket)
|
||||
* `wg` — WireGuard inter-hive mesh setup helpers (`services.hyperhive.swarm`)
|
||||
* `peer-config` — Generate the federation peer-config block for THIS hive — the nix a peer operator pastes into their `services.hyperhive.swarm.peers` 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
|
||||
* `peer-config` — Generate the federation peer-config block for THIS hive — the nix a peer operator pastes into their `services.hyperhive.swarm.peers` 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. 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
|
||||
* `choom` — Open an interactive Claude session inside an agent container
|
||||
* `stop` — Stop containers hive-wide in one operator action. Bare `hivectl stop` stops **everything** — all sub-agents plus the ci, forge, gateway, and matrix infra containers. Narrow it with scope flags: `--agents` (all sub-agents), `--ci` / `--forge` / `--gateway` / `--matrix` (named infra), and `--agent <name>` (repeatable) for specific sub-agents. Flags are additive (e.g. `--agents --matrix`). Requires the hive-c0re daemon (connects to the host admin socket). hive-c0re itself is never stopped — it services the request
|
||||
* `start` — Start containers hive-wide — the inverse of `hivectl stop`. Bare `hivectl start` starts everything back up; the same scope flags as `stop` narrow it (`--agents`, `--ci`, `--forge`, `--gateway`, `--matrix`, `--agent <name>`). Requires the hive-c0re daemon
|
||||
|
|
@ -349,13 +349,13 @@ Show the live mesh interface state (`wg show wg-hive`). Requires the mesh to be
|
|||
|
||||
## `hivectl peer-config`
|
||||
|
||||
Generate the federation peer-config block for THIS hive — the nix a peer operator pastes into their `services.hyperhive.swarm.peers` 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
|
||||
Generate the federation peer-config block for THIS hive — the nix a peer operator pastes into their `services.hyperhive.swarm.peers` 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. 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
|
||||
|
||||
**Usage:** `hivectl peer-config [OPTIONS] --domain <DOMAIN>`
|
||||
**Usage:** `hivectl peer-config [OPTIONS]`
|
||||
|
||||
###### **Options:**
|
||||
|
||||
* `--domain <DOMAIN>` — This hive's DNS domain — the `swarm.peers` attrset key the peer declares. Required: hivectl has no other source for it
|
||||
* `--domain <DOMAIN>` — 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
|
||||
* `--wg-address <WG_ADDRESS>` — This hive's WireGuard mesh address (e.g. `10.42.0.1/32`), emitted as `wireguardAddress`. Omit when not running the mesh
|
||||
* `--wg-endpoint <WG_ENDPOINT>` — This hive's public WireGuard endpoint (`host:port`), emitted as `wireguardEndpoint`. Omit when peers dial in / no mesh
|
||||
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,6 +114,12 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
|
|||
}
|
||||
HostRequest::Rebuild { name } => handle_rebuild(&coord, name).await?,
|
||||
HostRequest::List => HostResponse::list(lifecycle::list().await?),
|
||||
// The hive domain is injected into c0re's service env by
|
||||
// hive-c0re.nix (`HYPERHIVE_HIVE_DOMAIN`); surface it so the
|
||||
// operator CLI can fill in this hive's own identity.
|
||||
HostRequest::HiveDomain => {
|
||||
HostResponse::hive_domain(std::env::var("HYPERHIVE_HIVE_DOMAIN").ok())
|
||||
}
|
||||
HostRequest::Pending => HostResponse::pending(coord.approvals.pending()?),
|
||||
HostRequest::Approve { id } => {
|
||||
actions::approve(coord.clone(), *id).await?;
|
||||
|
|
@ -210,6 +216,7 @@ async fn handle_restart_all() -> Result<HostResponse> {
|
|||
error: Some(errors.join("; ")),
|
||||
agents: Some(ok_agents),
|
||||
approvals: None,
|
||||
domain: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -368,6 +375,7 @@ fn finish_lifecycle(ok_items: Vec<String>, errors: &[String]) -> HostResponse {
|
|||
error: Some(errors.join("; ")),
|
||||
agents: Some(ok_items),
|
||||
approvals: None,
|
||||
domain: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,11 @@ pub enum HostRequest {
|
|||
Rebuild { name: String },
|
||||
/// List managed containers.
|
||||
List,
|
||||
/// Report this hive's canonical DNS domain
|
||||
/// (`services.hyperhive.domain`), or `None` when unset. Lets the
|
||||
/// operator CLI fill in the hive's own identity (e.g. the federation
|
||||
/// peer-config block) without the operator retyping it.
|
||||
HiveDomain,
|
||||
/// List pending approval requests.
|
||||
Pending,
|
||||
/// Approve a pending request by id; the action runs immediately.
|
||||
|
|
@ -131,6 +136,10 @@ pub struct HostResponse {
|
|||
pub agents: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub approvals: Option<Vec<Approval>>,
|
||||
/// This hive's canonical DNS domain — `HiveDomain` result. `None`
|
||||
/// when the domain is unset (no `services.hyperhive.domain`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub domain: Option<String>,
|
||||
}
|
||||
|
||||
/// One row in the approval queue. `commit_ref` is overloaded per
|
||||
|
|
@ -218,6 +227,7 @@ impl HostResponse {
|
|||
error: None,
|
||||
agents: None,
|
||||
approvals: None,
|
||||
domain: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -228,6 +238,7 @@ impl HostResponse {
|
|||
error: Some(message.into()),
|
||||
agents: None,
|
||||
approvals: None,
|
||||
domain: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -238,6 +249,7 @@ impl HostResponse {
|
|||
error: None,
|
||||
agents: Some(agents),
|
||||
approvals: None,
|
||||
domain: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -248,6 +260,19 @@ impl HostResponse {
|
|||
error: None,
|
||||
agents: None,
|
||||
approvals: Some(approvals),
|
||||
domain: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `HiveDomain` result — this hive's canonical domain (or `None`).
|
||||
#[must_use]
|
||||
pub fn hive_domain(domain: Option<String>) -> Self {
|
||||
Self {
|
||||
ok: true,
|
||||
error: None,
|
||||
agents: None,
|
||||
approvals: None,
|
||||
domain,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue