121 lines
4.8 KiB
Rust
121 lines
4.8 KiB
Rust
//! Shared helpers used across hivectl's command handling: daemon
|
|
//! request/response plumbing (`daemon_request`, `render`,
|
|
//! `render_lifecycle`).
|
|
|
|
use std::path::Path;
|
|
|
|
use anyhow::{Context as _, Result, bail};
|
|
|
|
/// Parse a CLI-supplied agent/account name into a validated
|
|
/// [`hive_types::Ident`], mapping the parse error to an `anyhow` error that
|
|
/// names the offending input. Used at hivectl's `HostRequest` construction
|
|
/// sites so the wire `Ident` fields are built from validated names (the daemon
|
|
/// re-validates on deserialize; parsing here gives the operator an immediate,
|
|
/// local error instead of a round-trip rejection).
|
|
pub(crate) fn parse_ident(name: &str) -> Result<hive_types::Ident> {
|
|
hive_types::Ident::parse(name).map_err(|e| anyhow::anyhow!("invalid agent name {name:?}: {e}"))
|
|
}
|
|
|
|
/// Send a provisioning request to the daemon and print its result lines.
|
|
/// The daemon owns the provisioning logic; hivectl just relays the outcome,
|
|
/// prefixing any error with `label` (e.g. `forge`).
|
|
pub(crate) async fn daemon_request(
|
|
socket: &Path,
|
|
req: hive_host_sock::HostRequest,
|
|
label: &str,
|
|
) -> Result<()> {
|
|
// No extra context on the request: `client::request` already names the
|
|
// socket and classifies the failure, and wrapping it here would put a
|
|
// vaguer line on top of the actionable one.
|
|
let resp = crate::client::request(socket, req).await?;
|
|
if !resp.ok {
|
|
bail!(
|
|
"{label}: {}",
|
|
resp.error.as_deref().unwrap_or("unknown error")
|
|
);
|
|
}
|
|
for line in &resp.messages {
|
|
println!("{line}");
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Pretty-print a `HostResponse` as JSON and bail on failure. Used by the
|
|
/// agent-lifecycle verbs that just relay a daemon result verbatim.
|
|
pub(crate) fn render(resp: hive_host_sock::HostResponse) -> Result<()> {
|
|
println!("{}", serde_json::to_string_pretty(&resp)?);
|
|
if !resp.ok {
|
|
bail!(resp.error.unwrap_or_else(|| "request failed".to_owned()));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Render a hive-wide stop/start/restart response: one `<verb>: <name>`
|
|
/// line per touched container, then surface any aggregated per-target
|
|
/// failure as a non-zero exit. `verb` is the past-tense word printed per
|
|
/// item (`stopped` / `started`).
|
|
pub(crate) fn render_lifecycle(resp: &hive_host_sock::HostResponse, verb: &str) -> Result<()> {
|
|
let items = resp.agents.as_deref().unwrap_or(&[]);
|
|
if items.is_empty() {
|
|
println!("{verb}: nothing matched the requested scope");
|
|
} else {
|
|
for item in items {
|
|
println!("{verb}: {item}");
|
|
}
|
|
}
|
|
if !resp.ok {
|
|
bail!(
|
|
"{verb}: {}",
|
|
resp.error.as_deref().unwrap_or("unknown error")
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Query this hive's domain + browser-facing web URLs (`HostRequest::Urls`).
|
|
///
|
|
/// `Ok(None)` = the daemon answered and has nothing to report; `Err` = it was
|
|
/// never reached, and the error carries the actionable connect hint (see
|
|
/// [`crate::client::request`]). Keep the two apart: collapsing the error into
|
|
/// `None` here is what made a permission problem on the socket read as "is
|
|
/// hive-c0re running?", sending the operator to fix the wrong thing.
|
|
pub(crate) async fn query_hive_urls(socket: &Path) -> Result<Option<hive_host_sock::HiveUrls>> {
|
|
Ok(
|
|
crate::client::request(socket, hive_host_sock::HostRequest::Urls)
|
|
.await?
|
|
.urls,
|
|
)
|
|
}
|
|
|
|
/// True when `name` matches an existing hyperhive agent — i.e. it has a
|
|
/// persistent state dir under the agents root. The state dir (not the
|
|
/// live container list) is the test, so kept-state tombstones still
|
|
/// resolve as agents: re-provisioning a destroyed-but-kept agent should
|
|
/// drop its token into the existing state tree.
|
|
///
|
|
/// Asks the daemon rather than stat-ing the path. hivectl used to check
|
|
/// locally, but the agents root is `0700` and owned by the daemon's
|
|
/// user, so an operator without root got EACCES on traversal — which
|
|
/// this function then had to report as "needs root", turning every
|
|
/// caller's existence guard into a permission error. The daemon owns
|
|
/// that directory and answers the same question over the socket, which
|
|
/// operators can already reach via the `hive-admin` group, so the guard
|
|
/// works sudoless and the remaining root requirements (if any) surface
|
|
/// where they actually are.
|
|
pub(crate) async fn agent_exists(socket: &Path, name: &str) -> Result<bool> {
|
|
let resp = crate::client::request(
|
|
socket,
|
|
hive_host_sock::HostRequest::AgentExists {
|
|
name: parse_ident(name)?,
|
|
},
|
|
)
|
|
.await?;
|
|
if !resp.ok {
|
|
bail!(
|
|
"check whether {name:?} is an agent: {}",
|
|
resp.error.as_deref().unwrap_or("unknown error")
|
|
);
|
|
}
|
|
resp.agent_exists
|
|
.context("daemon answered the agent-exists check without a result")
|
|
}
|