The dashboard, its API, the swagger UI and the agent routes move to their own vhost, `swarm.dashboardDomain` = `<hive>.hives.<swarm.domain>`, under the swarm domain so authelia's session cookie reaches it. `/`, `/api/` and `/api/docs/` ask authelia through `auth_request`, and authelia's `operatorsOnly` rule for each hive's dashboard name lets only the admins group in. The hive domain keeps matrix discovery and redirects every other path to the new name. The name rides as a SAN on the hive's own gateway leaf: the per-hive bao role allows it, and hive-tls asks for it and checks coverage. HTTP Basic auth is gone: the htpasswd writer and its test, the `hivectl gateway` verbs and host-socket requests, the `gateway.auth` options (now a removed-option error), the unauthorized page and the bcrypt dependency. tmpfiles deletes a leftover gateway.htpasswd, which held bcrypt hashes at 0644. c0re hands out dashboard and agent URLs on the new name (HYPERHIVE_DASHBOARD_DOMAIN), and the swarm controller's hive roster carries each hive's dashboard name for the swarm UI's hive list. Closes #3469
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` / `quota`).
|
|
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")
|
|
}
|