hive: stop controlling the swarm's forge and matrix containers
The forge and matrix are swarm services; per mara's ruling on #5475 (shape a) a hive no longer starts, stops or restarts them. - hive-priv-sock: InfraContainer keeps only Gateway; InfraTarget goes, service_unit maps the gateway to nginx.service directly. A ControlInfraContainer naming Forge/Matrix fails to decode. - hive-host-sock: LifecycleScope drops forge/matrix and refuses unknown fields, so an older hivectl's --forge is an error rather than an all-false scope meaning everything. - hive-c0re: scoped_infra selects only the gateway. - hivectl: --forge/--matrix removed from stop/start/restart. - docs: services.md says how the forge and matrix are managed; security.md, coordinator.md and hivectl-cli.md follow. - hive-host-sock, hivectl/power.rs: Stop and RestartScoped doc comments name the gateway (host nginx.service), not infra containers.
This commit is contained in:
parent
be92bc0835
commit
d4f3d1ef8e
12 changed files with 177 additions and 203 deletions
|
|
@ -38,15 +38,14 @@ pub const AGENT_PREFIX: &str = "h-";
|
|||
///
|
||||
/// ⚠️ This is the *container-name* allowlist, not the lifecycle one:
|
||||
/// [`InfraContainer`] is what gates
|
||||
/// [`PrivRequest::ControlInfraContainer`], and it has one variant more than
|
||||
/// this list (the gateway). Keep the distinction — a name that belongs to
|
||||
/// no container has no business reaching a `nixos-container` call.
|
||||
/// [`PrivRequest::ControlInfraContainer`], and no name in this list has a
|
||||
/// variant there: the forge and matrix are swarm services, started and
|
||||
/// stopped by systemd on the host that runs them, never from a hive.
|
||||
pub const SIBLING_CONTAINERS: &[&str] = &["hive-forge", "hive-matrix"];
|
||||
|
||||
/// Lifecycle verb for [`PrivRequest::ControlInfraContainer`]. Maps directly
|
||||
/// to `systemctl <verb> <unit>`, where the unit comes from
|
||||
/// [`InfraContainer::service_unit`] — usually `container@<name>.service`,
|
||||
/// but not always (see [`InfraTarget`]).
|
||||
/// [`InfraContainer::service_unit`].
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum InfraAction {
|
||||
|
|
@ -65,79 +64,44 @@ impl InfraAction {
|
|||
}
|
||||
}
|
||||
|
||||
/// A hive infrastructure container that can be controlled (start / stop /
|
||||
/// A hive infrastructure service 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 (`"Forge"`, `"Gateway"`, …); it's an internal protocol (both
|
||||
/// ends rebuild together) so it needn't match the container name.
|
||||
/// [`name`](Self::name) is the separate stable identity string
|
||||
/// (`hive-forge`), and [`service_unit`](Self::service_unit) the systemd unit
|
||||
/// it actually resolves to.
|
||||
/// unknown or unsafe target is *unrepresentable* rather than caught by a
|
||||
/// runtime check. Notably absent: `hive-c0re`, whose stop would sever the
|
||||
/// daemon socket, and the swarm's forge and matrix, which a hive does not
|
||||
/// own. The c0re↔hive-priv wire form uses serde's default variant naming
|
||||
/// (`"Gateway"`); it's an internal protocol (both ends rebuild together)
|
||||
/// so it needn't match the service name. [`name`](Self::name) is the
|
||||
/// separate stable identity string (`hive-gateway`), and
|
||||
/// [`service_unit`](Self::service_unit) the systemd unit it resolves to.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum InfraContainer {
|
||||
Forge,
|
||||
Gateway,
|
||||
Matrix,
|
||||
}
|
||||
|
||||
/// What an [`InfraContainer`] resolves to on the host — i.e. the thing a
|
||||
/// lifecycle verb actually acts on.
|
||||
///
|
||||
/// The gateway is why this exists: its nginx + dnsmasq are host
|
||||
/// services, so "restart the gateway" means a plain host unit. Every
|
||||
/// other variant is a container.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum InfraTarget {
|
||||
/// An nspawn container, controlled via `container@<name>.service`.
|
||||
Container(&'static str),
|
||||
/// A plain host unit. There is no machine to enter: no
|
||||
/// `nixos-container` verb.
|
||||
HostUnit(&'static str),
|
||||
}
|
||||
|
||||
impl InfraContainer {
|
||||
/// Every controllable infra target. A superset of
|
||||
/// [`SIBLING_CONTAINERS`] — not all of these are containers (see the
|
||||
/// test).
|
||||
pub const ALL: [InfraContainer; 3] = [
|
||||
InfraContainer::Forge,
|
||||
InfraContainer::Gateway,
|
||||
InfraContainer::Matrix,
|
||||
];
|
||||
/// Every controllable infra target.
|
||||
pub const ALL: [InfraContainer; 1] = [InfraContainer::Gateway];
|
||||
|
||||
/// Stable identity string, e.g. `hive-forge`. This is what the operator
|
||||
/// types, what the dashboard displays, and what [`FromStr`](std::str::FromStr)
|
||||
/// parses — it stays `hive-gateway` even though the gateway is no
|
||||
/// longer a container, because it names the *service*, not its
|
||||
/// implementation. (Distinct from the serde wire form, which is the
|
||||
/// default variant name `"Forge"`.)
|
||||
/// Stable identity string, e.g. `hive-gateway`. This is what the
|
||||
/// operator types, what the dashboard displays, and what
|
||||
/// [`FromStr`](std::str::FromStr) parses. It names the *service*, not
|
||||
/// its implementation: the gateway is the host's nginx, not a
|
||||
/// container. (Distinct from the serde wire form, which is the default
|
||||
/// variant name `"Gateway"`.)
|
||||
#[must_use]
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
InfraContainer::Forge => "hive-forge",
|
||||
InfraContainer::Gateway => "hive-gateway",
|
||||
InfraContainer::Matrix => "hive-matrix",
|
||||
}
|
||||
}
|
||||
|
||||
/// Where this target lives on the host.
|
||||
#[must_use]
|
||||
pub fn target(self) -> InfraTarget {
|
||||
match self {
|
||||
InfraContainer::Gateway => InfraTarget::HostUnit("nginx.service"),
|
||||
other => InfraTarget::Container(other.name()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The systemd unit a lifecycle verb acts on.
|
||||
#[must_use]
|
||||
pub fn service_unit(self) -> String {
|
||||
match self.target() {
|
||||
InfraTarget::Container(name) => format!("container@{name}.service"),
|
||||
InfraTarget::HostUnit(unit) => unit.to_owned(),
|
||||
pub fn service_unit(self) -> &'static str {
|
||||
match self {
|
||||
InfraContainer::Gateway => "nginx.service",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -145,7 +109,7 @@ impl InfraContainer {
|
|||
impl std::str::FromStr for InfraContainer {
|
||||
type Err = ();
|
||||
|
||||
/// Parse an infra name (`hive-forge`, …) into a variant. `Err(())` for
|
||||
/// Parse an infra name (`hive-gateway`) into a variant. `Err(())` for
|
||||
/// anything that isn't one.
|
||||
fn from_str(s: &str) -> Result<Self, ()> {
|
||||
Self::ALL.into_iter().find(|c| c.name() == s).ok_or(())
|
||||
|
|
@ -415,13 +379,13 @@ pub enum PrivRequest {
|
|||
paused: bool,
|
||||
},
|
||||
|
||||
/// Start / stop a hive infrastructure container on the host via
|
||||
/// `systemctl <action> container@<container>.service`. The
|
||||
/// Start / stop a hive infrastructure service on the host via
|
||||
/// `systemctl <action> <unit>`, the unit derived from the variant. The
|
||||
/// [`InfraContainer`] enum is the allowlist — serde rejects unknown /
|
||||
/// unsafe names (notably `hive-c0re`, which has no variant) at the wire
|
||||
/// boundary, so no root-side `.contains()` check is needed. Serves the
|
||||
/// hive-wide `hivectl stop` / `hivectl start` flow and the dashboard's
|
||||
/// operator-only infra panel. No agent-facing path exists.
|
||||
/// unsafe names (notably `hive-c0re`, the forge and the matrix, which
|
||||
/// have no variant) at the wire boundary, so no root-side `.contains()`
|
||||
/// check is needed. Serves the hive-wide `hivectl stop` / `start` /
|
||||
/// `restart` flow. No agent-facing path exists.
|
||||
ControlInfraContainer {
|
||||
container: InfraContainer,
|
||||
action: InfraAction,
|
||||
|
|
@ -695,65 +659,43 @@ pub enum PrivEvent {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{InfraContainer, InfraTarget, SIBLING_CONTAINERS};
|
||||
use super::{InfraContainer, SIBLING_CONTAINERS};
|
||||
|
||||
#[test]
|
||||
fn infra_control_allowlist_excludes_c0re_includes_matrix() {
|
||||
fn sibling_container_allowlist_excludes_c0re() {
|
||||
// SIBLING_CONTAINERS is the authoritative allowlist for the
|
||||
// requests that name a container as a string. hive-c0re must NEVER
|
||||
// be in it — stopping the daemon would sever the socket the request
|
||||
// arrived on.
|
||||
assert!(!SIBLING_CONTAINERS.contains(&"hive-c0re"));
|
||||
// hive-matrix IS controllable (operator can stop/start/restart it).
|
||||
assert!(SIBLING_CONTAINERS.contains(&"hive-matrix"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn infra_container_enum_matches_sibling_containers() {
|
||||
// The two lists are the same set *minus the things that aren't
|
||||
// containers*: every `Container` variant must appear in
|
||||
// SIBLING_CONTAINERS and vice versa, so a `nixos-container` verb can
|
||||
// never be pointed at a name that has no machine behind it.
|
||||
let mut from_enum: Vec<&str> = InfraContainer::ALL
|
||||
.iter()
|
||||
.filter_map(|c| match c.target() {
|
||||
InfraTarget::Container(name) => Some(name),
|
||||
InfraTarget::HostUnit(_) => None,
|
||||
})
|
||||
.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);
|
||||
// The gateway is the one that is not: a host unit, and absent from
|
||||
// the container-name allowlist.
|
||||
fn infra_control_names_no_c0re_or_swarm_service() {
|
||||
// Neither the daemon nor the swarm's forge and matrix have a
|
||||
// variant. Control: the gateway's name does parse.
|
||||
for name in ["hive-c0re", "hive-forge", "hive-matrix"] {
|
||||
assert!(name.parse::<InfraContainer>().is_err(), "{name}");
|
||||
}
|
||||
assert_eq!(
|
||||
InfraContainer::Gateway.target(),
|
||||
InfraTarget::HostUnit("nginx.service")
|
||||
"hive-gateway".parse::<InfraContainer>(),
|
||||
Ok(InfraContainer::Gateway)
|
||||
);
|
||||
assert!(!SIBLING_CONTAINERS.contains(&"hive-gateway"));
|
||||
// hive-c0re has no variant — unrepresentable, can't be controlled.
|
||||
assert!("hive-c0re".parse::<InfraContainer>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn infra_container_name_round_trips() {
|
||||
// `name` is the single source of truth for the operator-facing form
|
||||
// (FromStr keys off it), so a name→variant→name round-trip proves
|
||||
// the mapping is consistent in both directions. It holds for the
|
||||
// gateway too: the name is still recognised, it's the *permission*
|
||||
// that differs (see below), not the parse.
|
||||
// the mapping is consistent in both directions.
|
||||
for c in InfraContainer::ALL {
|
||||
assert_eq!(c.name().parse::<InfraContainer>(), Ok(c));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_unit_wraps_containers_but_not_host_units() {
|
||||
assert_eq!(
|
||||
InfraContainer::Forge.service_unit(),
|
||||
"container@hive-forge.service"
|
||||
);
|
||||
fn gateway_resolves_to_host_nginx() {
|
||||
assert_eq!(InfraContainer::Gateway.service_unit(), "nginx.service");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue