read privateUsers from the closure record at start, and move the data owner option under hive-controller
This commit is contained in:
parent
1fd591d28b
commit
f5a609911c
1 changed files with 208 additions and 97 deletions
|
|
@ -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());
|
||||
|
|
|
|||
Loading…
Reference in a new issue