hive-sh4re: type infra containers as an InfraContainer enum
Replace the stringly-typed infra-control path with an InfraContainer enum
(Ci/Forge/Gateway/Matrix). The variants are the allowlist: serde rejects any
unknown or unsafe name (hive-c0re has no variant) at the wire boundary, so
hive-priv no longer needs a root-side SIBLING_CONTAINERS.contains() check on
ControlInfraContainer — the type enforces it, and 'the daemon can't stop
itself' is a compile-time guarantee.
- priv_proto: InfraContainer enum; manual Serialize/Deserialize + FromStr +
unit_name() all key off one mapping, so the wire form ('hive-ci', …) is
unchanged and there's no drift. ControlInfraContainer.container: String ->
InfraContainer.
- hive-priv / priv_client / server.rs: thread the enum; scoped_infra returns
Vec<InfraContainer>; the control handler uses unit_name().
- agent_server: the infra_admin restart gate parses the name via FromStr
instead of a slice .contains().
- SIBLING_CONTAINERS stays (validate_container_name/_system_name still use it
for journals / general container validation); a test keeps the enum and the
slice in lockstep.
This commit is contained in:
parent
6b1dbebe5a
commit
cd025b3790
5 changed files with 146 additions and 71 deletions
|
|
@ -535,10 +535,11 @@ async fn handle_restart_child(coord: &Arc<Coordinator>, agent: &str, name: &str)
|
||||||
// Infra-container restart: an agent holding the `infra_admin`
|
// Infra-container restart: an agent holding the `infra_admin`
|
||||||
// capability can restart a hive infrastructure container (hive-ci /
|
// capability can restart a hive infrastructure container (hive-ci /
|
||||||
// hive-gateway / hive-forge / hive-matrix) by passing its name to the
|
// hive-gateway / hive-forge / hive-matrix) by passing its name to the
|
||||||
// same restart tool. These names are never agent children, so this
|
// same restart tool. The `InfraContainer` enum parse both recognises
|
||||||
// branch is disjoint from the child-restart path below.
|
// these (never agent children, so disjoint from the child path below)
|
||||||
if hive_sh4re::priv_proto::SIBLING_CONTAINERS.contains(&name) {
|
// and yields the typed value the restart path needs.
|
||||||
return handle_restart_infra(coord, agent, name).await;
|
if let Ok(container) = name.parse::<hive_sh4re::priv_proto::InfraContainer>() {
|
||||||
|
return handle_restart_infra(coord, agent, container).await;
|
||||||
}
|
}
|
||||||
if let Some(err) = require_child(agent, name, "restart") {
|
if let Some(err) = require_child(agent, name, "restart") {
|
||||||
return err;
|
return err;
|
||||||
|
|
@ -556,15 +557,16 @@ async fn handle_restart_child(coord: &Arc<Coordinator>, agent: &str, name: &str)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Restart a hive infrastructure container on behalf of an agent that
|
/// Restart a hive infrastructure container on behalf of an agent that
|
||||||
/// holds the `infra_admin` capability. The container name is already
|
/// holds the `infra_admin` capability. The `container` is already a valid
|
||||||
/// known to be in `SIBLING_CONTAINERS`; this gates on the
|
/// [`InfraContainer`] (the caller parsed it); this gates on the capability
|
||||||
/// capability and routes the systemctl restart through hive-priv (which
|
/// and routes the systemctl restart through hive-priv. Direct, not
|
||||||
/// re-validates the name root-side). Direct, not approval-gated.
|
/// approval-gated.
|
||||||
async fn handle_restart_infra(
|
async fn handle_restart_infra(
|
||||||
coord: &Arc<Coordinator>,
|
coord: &Arc<Coordinator>,
|
||||||
agent: &str,
|
agent: &str,
|
||||||
container: &str,
|
container: hive_sh4re::priv_proto::InfraContainer,
|
||||||
) -> AgentResponse {
|
) -> AgentResponse {
|
||||||
|
let name = container.unit_name();
|
||||||
// Record the attempt in the operator-visible privileged-action audit
|
// Record the attempt in the operator-visible privileged-action audit
|
||||||
// trail, then emit a live `AuditEntryAdded` so the dashboard audit view
|
// trail, then emit a live `AuditEntryAdded` so the dashboard audit view
|
||||||
// appends it off `/dashboard/stream`. Best-effort: `record` returns the
|
// appends it off `/dashboard/stream`. Best-effort: `record` returns the
|
||||||
|
|
@ -572,27 +574,26 @@ async fn handle_restart_infra(
|
||||||
// row so the stored + streamed views can't drift. `action` is stable so
|
// row so the stored + streamed views can't drift. `action` is stable so
|
||||||
// the dashboard can group/filter.
|
// the dashboard can group/filter.
|
||||||
let audit = |outcome: crate::audit_log::AuditOutcome, detail: Option<&str>| {
|
let audit = |outcome: crate::audit_log::AuditOutcome, detail: Option<&str>| {
|
||||||
if let Some(entry) =
|
if let Some(entry) = coord
|
||||||
coord
|
.audit_log
|
||||||
.audit_log
|
.record(agent, "restart_infra", name, outcome, detail)
|
||||||
.record(agent, "restart_infra", container, outcome, detail)
|
|
||||||
{
|
{
|
||||||
coord.emit_audit_entry(entry);
|
coord.emit_audit_entry(entry);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if !crate::capabilities::has_cap(agent, hive_sh4re::Capability::InfraAdmin) {
|
if !crate::capabilities::has_cap(agent, hive_sh4re::Capability::InfraAdmin) {
|
||||||
tracing::warn!(%agent, %container, "agent: infra restart denied (no infra_admin capability)");
|
tracing::warn!(%agent, %name, "agent: infra restart denied (no infra_admin capability)");
|
||||||
audit(
|
audit(
|
||||||
crate::audit_log::AuditOutcome::Err,
|
crate::audit_log::AuditOutcome::Err,
|
||||||
Some("denied: missing infra_admin capability"),
|
Some("denied: missing infra_admin capability"),
|
||||||
);
|
);
|
||||||
return AgentResponse::Err {
|
return AgentResponse::Err {
|
||||||
message: format!(
|
message: format!(
|
||||||
"restarting infra container `{container}` requires the `infra_admin` capability"
|
"restarting infra container `{name}` requires the `infra_admin` capability"
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
tracing::info!(%agent, %container, "agent: restart infra container");
|
tracing::info!(%agent, %name, "agent: restart infra container");
|
||||||
match crate::priv_client::restart_infra_container(container).await {
|
match crate::priv_client::restart_infra_container(container).await {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
audit(crate::audit_log::AuditOutcome::Ok, None);
|
audit(crate::audit_log::AuditOutcome::Ok, None);
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@
|
||||||
|
|
||||||
use anyhow::{Context as _, Result, bail};
|
use anyhow::{Context as _, Result, bail};
|
||||||
use hive_sh4re::priv_proto::{
|
use hive_sh4re::priv_proto::{
|
||||||
BindMount, InfraAction, JournalQuery, NetworkIsolation, PRIV_SOCK, PrivEvent, PrivRequest,
|
BindMount, InfraAction, InfraContainer, JournalQuery, NetworkIsolation, PRIV_SOCK, PrivEvent,
|
||||||
PrivResponse, PrivStream,
|
PrivRequest, PrivResponse, PrivStream,
|
||||||
};
|
};
|
||||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||||
use tokio::net::UnixStream;
|
use tokio::net::UnixStream;
|
||||||
|
|
@ -288,21 +288,17 @@ pub async fn restart_matrix_daemon(agent_name: &str) -> Result<()> {
|
||||||
/// re-validates `container` against its root-side allowlist; callers must
|
/// re-validates `container` against its root-side allowlist; callers must
|
||||||
/// already have checked the requesting agent holds the `infra_admin`
|
/// already have checked the requesting agent holds the `infra_admin`
|
||||||
/// capability.
|
/// capability.
|
||||||
pub async fn restart_infra_container(container: &str) -> Result<()> {
|
pub async fn restart_infra_container(container: InfraContainer) -> Result<()> {
|
||||||
control_infra_container(container, InfraAction::Restart).await
|
control_infra_container(container, InfraAction::Restart).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start / stop / restart a hive infrastructure container (`hive-ci`,
|
/// Start / stop / restart a hive infrastructure container (`hive-ci`,
|
||||||
/// `hive-gateway`, `hive-forge`, `hive-matrix`) on the host via `systemctl
|
/// `hive-gateway`, `hive-forge`, `hive-matrix`) on the host via `systemctl
|
||||||
/// <action> container@<container>.service`. hive-priv re-validates
|
/// <action> container@<container>.service`. The [`InfraContainer`] enum is
|
||||||
/// `container` against its root-side allowlist (`SIBLING_CONTAINERS`). Used
|
/// the allowlist — hive-priv needs no name re-validation. Used by the
|
||||||
/// by the hive-wide `hivectl stop` / `hivectl start` flow.
|
/// hive-wide `hivectl stop` / `hivectl start` flow.
|
||||||
pub async fn control_infra_container(container: &str, action: InfraAction) -> Result<()> {
|
pub async fn control_infra_container(container: InfraContainer, action: InfraAction) -> Result<()> {
|
||||||
ok(call(&PrivRequest::ControlInfraContainer {
|
ok(call(&PrivRequest::ControlInfraContainer { container, action }).await?)
|
||||||
container: container.to_owned(),
|
|
||||||
action,
|
|
||||||
})
|
|
||||||
.await?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure the agent's persistent state root is a btrfs subvolume when the
|
/// Ensure the agent's persistent state root is a btrfs subvolume when the
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use hive_sh4re::priv_proto::InfraAction;
|
use hive_sh4re::priv_proto::{InfraAction, InfraContainer};
|
||||||
use hive_sh4re::{HostRequest, HostResponse, LifecycleScope};
|
use hive_sh4re::{HostRequest, HostResponse, LifecycleScope};
|
||||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||||
use tokio::net::{UnixListener, UnixStream};
|
use tokio::net::{UnixListener, UnixStream};
|
||||||
|
|
@ -229,7 +229,7 @@ async fn handle_restart_all() -> Result<HostResponse> {
|
||||||
async fn handle_stop(
|
async fn handle_stop(
|
||||||
coord: &Arc<Coordinator>,
|
coord: &Arc<Coordinator>,
|
||||||
agents: &[String],
|
agents: &[String],
|
||||||
infra: &[&str],
|
infra: &[InfraContainer],
|
||||||
graceful: bool,
|
graceful: bool,
|
||||||
) -> Result<HostResponse> {
|
) -> Result<HostResponse> {
|
||||||
tracing::info!(?agents, ?infra, graceful, "stop");
|
tracing::info!(?agents, ?infra, graceful, "stop");
|
||||||
|
|
@ -267,11 +267,12 @@ async fn handle_stop(
|
||||||
}
|
}
|
||||||
|
|
||||||
for &container in infra {
|
for &container in infra {
|
||||||
|
let name = container.unit_name();
|
||||||
match crate::priv_client::control_infra_container(container, InfraAction::Stop).await {
|
match crate::priv_client::control_infra_container(container, InfraAction::Stop).await {
|
||||||
Ok(()) => ok_items.push(container.to_owned()),
|
Ok(()) => ok_items.push(name.to_owned()),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!(%container, error = ?e, "stop: infra stop failed");
|
tracing::warn!(%name, error = ?e, "stop: infra stop failed");
|
||||||
errors.push(format!("{container}: {e:#}"));
|
errors.push(format!("{name}: {e:#}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -283,17 +284,18 @@ async fn handle_stop(
|
||||||
/// inverse of [`handle_stop`]. Infra comes up before agents so the agents
|
/// inverse of [`handle_stop`]. Infra comes up before agents so the agents
|
||||||
/// find forge/matrix/gateway ready. Per-target failures aggregated. Callers
|
/// find forge/matrix/gateway ready. Per-target failures aggregated. Callers
|
||||||
/// resolve the [`LifecycleScope`] to these explicit name lists up front.
|
/// resolve the [`LifecycleScope`] to these explicit name lists up front.
|
||||||
async fn handle_start(agents: &[String], infra: &[&str]) -> Result<HostResponse> {
|
async fn handle_start(agents: &[String], infra: &[InfraContainer]) -> Result<HostResponse> {
|
||||||
tracing::info!(?agents, ?infra, "start");
|
tracing::info!(?agents, ?infra, "start");
|
||||||
let mut ok_items: Vec<String> = Vec::new();
|
let mut ok_items: Vec<String> = Vec::new();
|
||||||
let mut errors: Vec<String> = Vec::new();
|
let mut errors: Vec<String> = Vec::new();
|
||||||
|
|
||||||
for &container in infra {
|
for &container in infra {
|
||||||
|
let name = container.unit_name();
|
||||||
match crate::priv_client::control_infra_container(container, InfraAction::Start).await {
|
match crate::priv_client::control_infra_container(container, InfraAction::Start).await {
|
||||||
Ok(()) => ok_items.push(container.to_owned()),
|
Ok(()) => ok_items.push(name.to_owned()),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!(%container, error = ?e, "start: infra start failed");
|
tracing::warn!(%name, error = ?e, "start: infra start failed");
|
||||||
errors.push(format!("{container}: {e:#}"));
|
errors.push(format!("{name}: {e:#}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -333,23 +335,23 @@ async fn scoped_agents(scope: &LifecycleScope) -> Result<Vec<String>> {
|
||||||
Ok(set.into_iter().collect())
|
Ok(set.into_iter().collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve which infra container names a scope targets. An "everything"
|
/// Resolve which infra containers a scope targets. An "everything" scope
|
||||||
/// scope (no flags set) selects all controllable infra; otherwise each set
|
/// (no flags set) selects all controllable infra; otherwise each set flag
|
||||||
/// flag maps to its container. Fixed order for deterministic output.
|
/// maps to its [`InfraContainer`]. Fixed order for deterministic output.
|
||||||
fn scoped_infra(scope: &LifecycleScope) -> Vec<&'static str> {
|
fn scoped_infra(scope: &LifecycleScope) -> Vec<InfraContainer> {
|
||||||
let everything = scope.is_everything();
|
let everything = scope.is_everything();
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
if everything || scope.ci {
|
if everything || scope.ci {
|
||||||
out.push("hive-ci");
|
out.push(InfraContainer::Ci);
|
||||||
}
|
}
|
||||||
if everything || scope.forge {
|
if everything || scope.forge {
|
||||||
out.push("hive-forge");
|
out.push(InfraContainer::Forge);
|
||||||
}
|
}
|
||||||
if everything || scope.gateway {
|
if everything || scope.gateway {
|
||||||
out.push("hive-gateway");
|
out.push(InfraContainer::Gateway);
|
||||||
}
|
}
|
||||||
if everything || scope.matrix {
|
if everything || scope.matrix {
|
||||||
out.push("hive-matrix");
|
out.push(InfraContainer::Matrix);
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,9 @@ use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::{Context as _, Result, bail};
|
use anyhow::{Context as _, Result, bail};
|
||||||
use hive_sh4re::priv_proto::{
|
use hive_sh4re::priv_proto::{
|
||||||
AGENT_PREFIX, AGENT_STATE_ROOT, BindMount, InfraAction, JournalQuery, MANAGER_NAME, META_DIR,
|
AGENT_PREFIX, AGENT_STATE_ROOT, BindMount, InfraAction, InfraContainer, JournalQuery,
|
||||||
NetworkIsolation, PRIV_SOCK, PrivEvent, PrivRequest, PrivResponse, PrivStream, PrivStreamLine,
|
MANAGER_NAME, META_DIR, NetworkIsolation, PRIV_SOCK, PrivEvent, PrivRequest, PrivResponse,
|
||||||
SIBLING_CONTAINERS,
|
PrivStream, PrivStreamLine, SIBLING_CONTAINERS,
|
||||||
};
|
};
|
||||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||||
use tokio::net::unix::OwnedWriteHalf;
|
use tokio::net::unix::OwnedWriteHalf;
|
||||||
|
|
@ -268,10 +268,9 @@ async fn exec(req: PrivRequest, writer: &mut OwnedWriteHalf) -> Result<(String,
|
||||||
restart_matrix_daemon(agent_name).await
|
restart_matrix_daemon(agent_name).await
|
||||||
}
|
}
|
||||||
|
|
||||||
PrivRequest::ControlInfraContainer {
|
PrivRequest::ControlInfraContainer { container, action } => {
|
||||||
ref container,
|
control_infra_container(container, action).await
|
||||||
action,
|
}
|
||||||
} => control_infra_container(container, action).await,
|
|
||||||
|
|
||||||
PrivRequest::EnsureAgentSubvolume { ref agent_name } => {
|
PrivRequest::EnsureAgentSubvolume { ref agent_name } => {
|
||||||
validate_agent_name(agent_name)?;
|
validate_agent_name(agent_name)?;
|
||||||
|
|
@ -430,17 +429,17 @@ async fn restart_matrix_daemon(agent_name: &str) -> Result<(String, String)> {
|
||||||
|
|
||||||
/// `ControlInfraContainer` — start/stop/restart a hive infrastructure
|
/// `ControlInfraContainer` — start/stop/restart a hive infrastructure
|
||||||
/// container via `systemctl <verb> container@<container>.service`. The
|
/// container via `systemctl <verb> container@<container>.service`. The
|
||||||
/// `container` is validated against `SIBLING_CONTAINERS` here, root-side;
|
/// [`InfraContainer`] enum is the allowlist: serde already rejected any
|
||||||
/// this is the authoritative allowlist (hive-c0re is never in it, so a
|
/// unknown / unsafe name (hive-c0re has no variant, so a stop can't sever
|
||||||
/// stop can't sever the daemon socket the request arrived on). Serves both
|
/// the daemon socket) at deserialisation, so no root-side `.contains()`
|
||||||
/// the hive-wide `hivectl stop`/`start` flow and an `infra_admin` agent's
|
/// check is needed here. Serves both the hive-wide `hivectl stop`/`start`
|
||||||
/// `restart` (action = Restart).
|
/// flow and an `infra_admin` agent's `restart` (action = Restart).
|
||||||
async fn control_infra_container(container: &str, action: InfraAction) -> Result<(String, String)> {
|
async fn control_infra_container(
|
||||||
if !SIBLING_CONTAINERS.contains(&container) {
|
container: InfraContainer,
|
||||||
bail!("container {container:?} is not a controllable hive infra container");
|
action: InfraAction,
|
||||||
}
|
) -> Result<(String, String)> {
|
||||||
let verb = action.systemctl_verb();
|
let verb = action.systemctl_verb();
|
||||||
let unit = format!("container@{container}.service");
|
let unit = format!("container@{}.service", container.unit_name());
|
||||||
let out = Command::new("systemctl")
|
let out = Command::new("systemctl")
|
||||||
.args([verb, &unit])
|
.args([verb, &unit])
|
||||||
.output()
|
.output()
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,59 @@ impl InfraAction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A hive infrastructure container that can be controlled (start / stop /
|
||||||
|
/// restart) via [`PrivRequest::ControlInfraContainer`]. The variants ARE
|
||||||
|
/// the allowlist: serde rejects any other value at the wire boundary, so an
|
||||||
|
/// unknown or unsafe target — notably `hive-c0re`, which has no variant and
|
||||||
|
/// would sever the daemon socket — is *unrepresentable* rather than caught
|
||||||
|
/// by a runtime check. The c0re↔hive-priv wire form uses serde's default
|
||||||
|
/// variant naming (`"Ci"`, `"Forge"`, …); it's an internal protocol (both
|
||||||
|
/// ends rebuild together) so it needn't match the container name.
|
||||||
|
/// [`unit_name`](Self::unit_name) is the separate systemd / container name
|
||||||
|
/// (`hive-ci`).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum InfraContainer {
|
||||||
|
Ci,
|
||||||
|
Forge,
|
||||||
|
Gateway,
|
||||||
|
Matrix,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InfraContainer {
|
||||||
|
/// Every controllable infra container. The source of truth that
|
||||||
|
/// [`SIBLING_CONTAINERS`] is kept consistent with (see the test).
|
||||||
|
pub const ALL: [InfraContainer; 4] = [
|
||||||
|
InfraContainer::Ci,
|
||||||
|
InfraContainer::Forge,
|
||||||
|
InfraContainer::Gateway,
|
||||||
|
InfraContainer::Matrix,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// The container / systemd-unit name, e.g. `hive-ci` →
|
||||||
|
/// `container@hive-ci.service`. (Distinct from the serde wire form,
|
||||||
|
/// which is the default variant name `"Ci"`.)
|
||||||
|
#[must_use]
|
||||||
|
pub fn unit_name(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
InfraContainer::Ci => "hive-ci",
|
||||||
|
InfraContainer::Forge => "hive-forge",
|
||||||
|
InfraContainer::Gateway => "hive-gateway",
|
||||||
|
InfraContainer::Matrix => "hive-matrix",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for InfraContainer {
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
/// Parse a container name (`hive-ci`, …) into a variant. Used to decide
|
||||||
|
/// whether an MCP `restart(<name>)` target is a controllable infra
|
||||||
|
/// container. `Err(())` for anything that isn't one.
|
||||||
|
fn from_str(s: &str) -> Result<Self, ()> {
|
||||||
|
Self::ALL.into_iter().find(|c| c.unit_name() == s).ok_or(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Host path of the meta flake. The flake ref for agent `<name>` is
|
/// Host path of the meta flake. The flake ref for agent `<name>` is
|
||||||
/// `{META_DIR}#{name}`, derived by `hive-priv` — never passed over the wire.
|
/// `{META_DIR}#{name}`, derived by `hive-priv` — never passed over the wire.
|
||||||
pub const META_DIR: &str = "/var/lib/hyperhive/meta";
|
pub const META_DIR: &str = "/var/lib/hyperhive/meta";
|
||||||
|
|
@ -325,15 +378,14 @@ pub enum PrivRequest {
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Start / stop / restart a hive infrastructure container on the host
|
/// Start / stop / restart a hive infrastructure container on the host
|
||||||
/// via `systemctl <action> container@<container>.service`. hive-priv
|
/// via `systemctl <action> container@<container>.service`. The
|
||||||
/// validates `container` against [`SIBLING_CONTAINERS`] root-side (the
|
/// [`InfraContainer`] enum is the allowlist — serde rejects unknown /
|
||||||
/// authoritative allowlist; `hive-c0re` is never in it). Serves both the
|
/// unsafe names (notably `hive-c0re`, which has no variant) at the wire
|
||||||
/// hive-wide `hivectl stop` / `hivectl start` flow and an `infra_admin`
|
/// boundary, so no root-side `.contains()` check is needed. Serves both
|
||||||
/// agent's `restart` (with `action = Restart`).
|
/// the hive-wide `hivectl stop` / `hivectl start` flow and an
|
||||||
|
/// `infra_admin` agent's `restart` (with `action = Restart`).
|
||||||
ControlInfraContainer {
|
ControlInfraContainer {
|
||||||
/// Infra container name (e.g. `hive-ci`); must be in
|
container: InfraContainer,
|
||||||
/// [`SIBLING_CONTAINERS`].
|
|
||||||
container: String,
|
|
||||||
action: InfraAction,
|
action: InfraAction,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -486,7 +538,7 @@ pub enum PrivEvent {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::SIBLING_CONTAINERS;
|
use super::{InfraContainer, SIBLING_CONTAINERS};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn infra_control_allowlist_excludes_c0re_includes_matrix() {
|
fn infra_control_allowlist_excludes_c0re_includes_matrix() {
|
||||||
|
|
@ -497,4 +549,29 @@ mod tests {
|
||||||
// hive-matrix IS controllable (operator can stop/start/restart it).
|
// hive-matrix IS controllable (operator can stop/start/restart it).
|
||||||
assert!(SIBLING_CONTAINERS.contains(&"hive-matrix"));
|
assert!(SIBLING_CONTAINERS.contains(&"hive-matrix"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn infra_container_enum_matches_sibling_containers() {
|
||||||
|
// The InfraContainer enum (the control-path allowlist) and the
|
||||||
|
// SIBLING_CONTAINERS slice (the general container-name validator)
|
||||||
|
// must list exactly the same four containers — they're separate
|
||||||
|
// surfaces for the same set, so keep them in lockstep.
|
||||||
|
let mut from_enum: Vec<&str> = InfraContainer::ALL.iter().map(|c| c.unit_name()).collect();
|
||||||
|
from_enum.sort_unstable();
|
||||||
|
let mut from_slice: Vec<&str> = SIBLING_CONTAINERS.to_vec();
|
||||||
|
from_slice.sort_unstable();
|
||||||
|
assert_eq!(from_enum, from_slice);
|
||||||
|
// hive-c0re has no variant — unrepresentable, can't be controlled.
|
||||||
|
assert!("hive-c0re".parse::<InfraContainer>().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn infra_container_name_round_trips() {
|
||||||
|
// `unit_name` is the single source of truth for the wire form (the
|
||||||
|
// serde impls + FromStr all key off it), so a name→variant→name
|
||||||
|
// round-trip proves the mapping is consistent in both directions.
|
||||||
|
for c in InfraContainer::ALL {
|
||||||
|
assert_eq!(c.unit_name().parse::<InfraContainer>(), Ok(c));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue