split hivectl main.rs into per-domain modules (#2509)
This commit is contained in:
parent
673aea4e50
commit
fc7720572b
16 changed files with 1922 additions and 1771 deletions
128
hivectl/src/util.rs
Normal file
128
hivectl/src/util.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
//! Shared helpers used across hivectl's command handling: daemon
|
||||
//! request/response plumbing (`daemon_request`, `render`,
|
||||
//! `render_lifecycle`) and password input (`resolve_password`).
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
|
||||
/// 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` / `github`).
|
||||
pub(crate) async fn daemon_request(
|
||||
socket: &Path,
|
||||
req: hive_host_sock::HostRequest,
|
||||
label: &str,
|
||||
) -> Result<()> {
|
||||
let resp = crate::client::request(socket, req)
|
||||
.await
|
||||
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
|
||||
if !resp.ok {
|
||||
bail!(
|
||||
"{label}: {}",
|
||||
resp.error.as_deref().unwrap_or("unknown error")
|
||||
);
|
||||
}
|
||||
for line in &resp.messages {
|
||||
println!("{line}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolve the password the caller asked for, or return `None` to fall
|
||||
/// back to a random throwaway. `--password <PW>` wins outright;
|
||||
/// `--password-stdin` reads one line off stdin (trailing newline
|
||||
/// stripped). Empty stdin is treated as an error so the caller doesn't
|
||||
/// silently provision an empty-password account.
|
||||
pub(crate) fn resolve_password(
|
||||
password: Option<&str>,
|
||||
password_stdin: bool,
|
||||
) -> Result<Option<String>> {
|
||||
if let Some(p) = password {
|
||||
return Ok(Some(p.to_owned()));
|
||||
}
|
||||
if password_stdin {
|
||||
use std::io::BufRead as _;
|
||||
let stdin = std::io::stdin();
|
||||
let mut line = String::new();
|
||||
stdin
|
||||
.lock()
|
||||
.read_line(&mut line)
|
||||
.context("read password from stdin")?;
|
||||
let trimmed = line.trim_end_matches(['\r', '\n']).to_owned();
|
||||
if trimmed.is_empty() {
|
||||
bail!("--password-stdin: empty input");
|
||||
}
|
||||
return Ok(Some(trimmed));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Pretty-print a `HostResponse` as JSON and bail on failure. Used by the
|
||||
/// agent-lifecycle + approval 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(())
|
||||
}
|
||||
|
||||
/// Best-effort query for this hive's domain + browser-facing web URLs
|
||||
/// (`HostRequest::Urls`). `None` when the daemon is unreachable.
|
||||
pub(crate) async fn query_hive_urls(socket: &Path) -> Option<hive_host_sock::HiveUrls> {
|
||||
crate::client::request(socket, hive_host_sock::HostRequest::Urls)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|r| r.urls)
|
||||
}
|
||||
|
||||
/// True when `name` matches an existing hyperhive agent — i.e. it has a
|
||||
/// persistent state dir under `/var/lib/hyperhive/agents/`. We use the
|
||||
/// state dir (not the live container list) so kept-state tombstones
|
||||
/// still resolve as agents — re-provisioning a destroyed-but-kept agent
|
||||
/// should still drop its token in the existing state tree.
|
||||
///
|
||||
/// Uses `try_exists()` rather than `Path::exists()` so a permission
|
||||
/// error reaching the agents root is surfaced, not collapsed into
|
||||
/// `false`. The agents root is `0700 hive-core`, so running hivectl
|
||||
/// without root yields EACCES on traversal — `Path::exists()` would
|
||||
/// silently report `false`, which callers turn into a misleading "no
|
||||
/// such agent" (or, for the create-user paths, a silent misclassify of
|
||||
/// a real agent as a non-agent account). Mapping EACCES to an explicit
|
||||
/// "needs root" error fixes that first-run footgun, where running a
|
||||
/// privileged verb without sudo reported as a missing agent.
|
||||
pub(crate) fn agent_exists(name: &str) -> Result<bool> {
|
||||
let root = hive_host_sock::agent_state_dir(name);
|
||||
match root.try_exists() {
|
||||
Ok(found) => Ok(found),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => bail!(
|
||||
"cannot read the agents root {} (permission denied) - this command needs root; \
|
||||
re-run with sudo",
|
||||
root.parent().unwrap_or(&root).display()
|
||||
),
|
||||
Err(e) => Err(e).with_context(|| format!("check agent state dir {}", root.display())),
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue