Watch
0
0
Fork
You've already forked hyperhive
0

read privateUsers from the closure record at start, and move the data owner option under hive-controller

This commit is contained in:
atlas 2026-11-01 16:18:34 +01:00 • committed by mara
commit f5a609911c

View file

@ -1,11 +1,12 @@
//! Agents that run in their own user namespace.
//!
//! The operator lists them in `services.hyperhive.userns.agents` and names the
//! one host uid:gid that owns their data on disk (`dataOwner`); the unit's
//! environment carries both, so only root decides who is namespaced. A listed
//! agent starts with `--private-users=pick`, a root idmapped rather than
//! chowned, and every bind mounted `owneridmap`: the owner of the bind's
//! target inside the container, the agent user, is the data owner on disk.
//! An agent's config sets `services.hyperhive.agent.user.privateUsers`; the
//! swarm builder records it, hive-c0re reads the record and sends the flag with
//! each start. The host's data owner (`dataOwner`, one uid:gid) comes from this
//! unit's environment. A namespaced agent starts with `--private-users=pick`, a
//! root idmapped rather than chowned, and every bind mounted `owneridmap`: the
//! owner of the bind's target inside the container, the agent user, is the data
//! owner on disk.
//!
//! Getting there moves the agent's data once. [`activate`] chowns its `state`,
//! `harness`, `work` and `claude` dirs to the data owner, never touching
@ -28,7 +29,6 @@ use rustix::io::Errno;
use crate::files::SOCKET_DIR_ROOT;
const AGENTS_ENV: &str = "HIVE_PRIV_USERNS_AGENTS";
const DATA_OWNER_ENV: &str = "HIVE_PRIV_USERNS_DATA_OWNER";
/// One `<owner> <uid> <gid>` file per agent: the data owner its data was moved
@ -49,7 +49,7 @@ pub(crate) struct Env<'a> {
pub(crate) socket_dir: &'a Path,
pub(crate) marker_root: &'a Path,
pub(crate) rootfs: &'a Path,
/// The data owner when the agent is listed, else `None`.
/// The data owner when the agent is to be namespaced, else `None`.
pub(crate) owner: Option<u32>,
pub(crate) stopped: bool,
/// Lowest uid taken for an agent's own: below it a dir belongs to root or
@ -78,22 +78,26 @@ fn chown_at(dir: BorrowedFd<'_>, name: &CStr, uid: u32, gid: u32) -> std::io::Re
)?)
}
/// The data owner for `agent` under the unit's environment: `None` when the
/// agent is not listed.
/// The data owner when `agent` is to run in a user namespace, else `None`.
///
/// `requested` is what hive-c0re sent from the closure record. `None` there
/// means it could not say, and the agent stays as it is: namespaced if
/// hive-priv already `moved` its data.
///
/// # Errors
/// The agent is listed and the data owner is missing or not a plain uid.
/// The agent is to be namespaced and the data owner is missing or not a plain
/// uid.
pub(crate) fn owner_for(
agents: Option<&str>,
requested: Option<bool>,
moved: bool,
owner: Option<&str>,
agent: &str,
) -> Result<Option<u32>> {
if !agents.is_some_and(|list| list.split_whitespace().any(|a| a == agent)) {
if !requested.unwrap_or(moved) {
return Ok(None);
}
let owner = owner.with_context(|| {
format!("{agent} is listed in {AGENTS_ENV}, but {DATA_OWNER_ENV} is not set")
})?;
let owner = owner
.with_context(|| format!("{agent} sets privateUsers, but {DATA_OWNER_ENV} is not set"))?;
let owner = owner
.trim()
.parse()
@ -101,30 +105,37 @@ pub(crate) fn owner_for(
Ok(Some(owner))
}
/// [`owner_for`] under the unit's environment.
/// [`owner_for`] under the unit's environment and the agent's marker.
///
/// # Errors
/// As [`owner_for`].
pub(crate) fn owner_for_agent(agent: &str) -> Result<Option<u32>> {
/// As [`owner_for`], or `agent` is not an agent name.
pub(crate) fn owner_for_agent(agent: &str, requested: Option<bool>) -> Result<Option<u32>> {
crate::shared::validate_agent_name(agent)?;
owner_for(
std::env::var(AGENTS_ENV).ok().as_deref(),
requested,
Path::new(MARKER_ROOT).join(agent).exists(),
std::env::var(DATA_OWNER_ENV).ok().as_deref(),
agent,
)
}
/// Make `agent`'s next start of `container` namespaced when it is listed, and
/// say whether it is. `binds` are the ones the start will mount.
/// Make `agent`'s next start of `container` namespaced when its config asks
/// for it, and say whether it is. `binds` are the ones the start will mount.
///
/// A container that is running, and whose data has not been moved yet, keeps
/// its current start: the move waits for a stopped container.
///
/// # Errors
/// The environment is inconsistent, the data owner changed since the data
/// moved, the agent was dropped from the list after it moved, or a chown or
/// mkdir fails.
pub(crate) fn activate(container: &str, agent: &str, binds: &[BindMount]) -> Result<bool> {
let owner = owner_for_agent(agent)?;
/// moved, the agent stopped asking for a namespace after it moved, or a chown
/// or mkdir fails.
pub(crate) fn activate(
container: &str,
agent: &str,
binds: &[BindMount],
requested: Option<bool>,
) -> Result<bool> {
let owner = owner_for_agent(agent, requested)?;
let rootfs = crate::container_config::container_rootfs(container);
let stopped = crate::rootfs::unit_is_stopped(container)?;
activate_in(
@ -153,8 +164,8 @@ pub(crate) fn activate_in(env: &Env<'_>, agent: &str, binds: &[BindMount]) -> Re
let Some(owner) = env.owner else {
if moved.is_some() {
bail!(
"{agent}: its data was moved to the userns data owner and it is no longer \
listed; list it again, or chown its dirs back and remove {}",
"{agent}: its data was moved to the userns data owner and its config no longer \
sets privateUsers; set it again, or chown its dirs back and remove {}",
marker.display()
);
}
@ -470,14 +481,25 @@ mod tests {
}
}
/// The flag decides; with no word from the record the marker does. The
/// refusal arms and the `None` arms are each other's controls.
#[test]
fn only_listed_agents_have_an_owner() {
let owner = |agents, owner| owner_for(agents, owner, "a");
assert_eq!(owner(Some("b a"), Some("60000")).unwrap(), Some(60000));
assert_eq!(owner(Some("b c"), Some("60000")).unwrap(), None);
assert_eq!(owner(None, None).unwrap(), None);
assert!(owner(Some("a"), None).is_err(), "listed without an owner");
assert!(owner(Some("a"), Some("root")).is_err(), "not a uid");
fn the_requested_flag_decides_and_without_it_the_marker_does() {
let owner = |requested, moved, env| owner_for(requested, moved, env, "a");
assert_eq!(
owner(Some(true), false, Some("60000")).unwrap(),
Some(60000)
);
assert_eq!(owner(Some(false), false, Some("60000")).unwrap(), None);
assert_eq!(owner(Some(false), true, Some("60000")).unwrap(), None);
assert_eq!(owner(None, true, Some("60000")).unwrap(), Some(60000));
assert_eq!(owner(None, false, Some("60000")).unwrap(), None);
assert!(owner(Some(true), false, None).is_err(), "no data owner");
assert!(owner(Some(true), false, Some("root")).is_err(), "not a uid");
assert!(
owner(Some(false), false, None).is_ok(),
"not asked, not needed"
);
}
/// The move chowns the four data dirs and what is in them and nothing
@ -552,7 +574,7 @@ mod tests {
}
#[test]
fn an_unlisted_agent_is_left_alone_and_a_moved_one_is_refused() {
fn an_agent_without_the_flag_is_left_alone_and_a_moved_one_is_refused() {
let root = tree();
let (answer, seen) = run(&root, None, true, "a", &[]);
assert!(!answer.unwrap() && seen.is_empty());