Refactor/project structure (#3085)

* refactor project structure

* environment-based default registry

* fix tests

* update build container

* use docker platform for iso build emulation

* simplify compat

* Fix docker platform spec in run-compat.sh

* handle riscv compat

* fix bug with dep error exists attr

* undo removal of sorting

* use qemu for iso stage

---------

Co-authored-by: Mariusz Kogen <k0gen@pm.me>
Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
Aiden McClelland
2025-12-22 13:39:38 -07:00
committed by GitHub
parent eda08d5b0f
commit 96ae532879
389 changed files with 744 additions and 4005 deletions

View File

@@ -0,0 +1,718 @@
use std::collections::BTreeMap;
use std::ffi::{OsStr, OsString, c_int};
use std::fs::File;
use std::io::{IsTerminal, Read};
use std::os::unix::process::{CommandExt, ExitStatusExt};
use std::path::{Path, PathBuf};
use std::process::{Command as StdCommand, Stdio};
use std::sync::Arc;
use nix::errno::Errno;
use nix::sched::CloneFlags;
use nix::unistd::Pid;
use signal_hook::consts::signal::*;
use termion::raw::IntoRawMode;
use tokio::sync::oneshot;
use crate::CAP_1_KiB;
use crate::service::effects::ContainerCliContext;
use crate::service::effects::prelude::*;
use crate::util::io::TermSize;
const FWD_SIGNALS: &[c_int] = &[
SIGABRT, SIGALRM, SIGCONT, SIGHUP, SIGINT, SIGIO, SIGPIPE, SIGPROF, SIGQUIT, SIGTERM, SIGTRAP,
SIGTSTP, SIGTTIN, SIGTTOU, SIGURG, SIGUSR1, SIGUSR2, SIGVTALRM,
];
pub fn kill_init(procfs: &Path, chroot: &Path) -> Result<(), Error> {
if chroot.join("proc/1").exists() {
let ns_id = procfs::process::Process::new_with_root(chroot.join("proc/1"))
.with_ctx(|_| (ErrorKind::Filesystem, "open subcontainer procfs"))?
.namespaces()
.with_ctx(|_| (ErrorKind::Filesystem, "read subcontainer pid 1 ns"))?
.0
.get(OsStr::new("pid"))
.or_not_found("pid namespace")?
.identifier;
for proc in procfs::process::all_processes_with_root(procfs)
.with_ctx(|_| (ErrorKind::Filesystem, "open procfs"))?
{
let proc = proc.with_ctx(|_| (ErrorKind::Filesystem, "read single process details"))?;
let pid = proc.pid();
if proc
.namespaces()
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("read pid {} ns", pid)))?
.0
.get(OsStr::new("pid"))
.map_or(false, |ns| ns.identifier == ns_id)
{
let pids = proc.read::<_, NSPid>("status").with_ctx(|_| {
(
ErrorKind::Filesystem,
lazy_format!("read pid {} NSpid", pid),
)
})?;
if pids.0.len() == 2 && pids.0[1] == 1 {
match nix::sys::signal::kill(
Pid::from_raw(pid),
Some(nix::sys::signal::SIGKILL),
) {
Err(Errno::ESRCH) => Ok(()),
a => a,
}
.with_ctx(|_| {
(
ErrorKind::Filesystem,
lazy_format!(
"kill pid {} (determined to be pid 1 in subcontainer)",
pid
),
)
})?;
}
}
}
nix::mount::umount(&chroot.join("proc"))
.with_ctx(|_| (ErrorKind::Filesystem, "unmounting subcontainer procfs"))?;
}
Ok(())
}
struct NSPid(Vec<i32>);
impl procfs::FromBufRead for NSPid {
fn from_buf_read<R: std::io::BufRead>(r: R) -> procfs::ProcResult<Self> {
for line in r.lines() {
let line = line?;
if let Some(row) = line.trim().strip_prefix("NSpid:") {
return Ok(Self(
row.trim()
.split_ascii_whitespace()
.map(|pid| pid.parse::<i32>())
.collect::<Result<Vec<_>, _>>()?,
));
}
}
Err(procfs::ProcError::Incomplete(None))
}
}
fn open_file_read(path: impl AsRef<Path>) -> Result<File, Error> {
File::open(&path).with_ctx(|_| {
(
ErrorKind::Filesystem,
lazy_format!("open r {}", path.as_ref().display()),
)
})
}
#[derive(Debug, Clone, Serialize, Deserialize, Parser)]
pub struct ExecParams {
#[arg(long)]
force_tty: bool,
#[arg(long)]
force_stderr_tty: bool,
#[arg(long)]
pty_size: Option<TermSize>,
#[arg(short, long)]
env: Vec<String>,
#[arg(long)]
env_file: Option<PathBuf>,
#[arg(short, long)]
workdir: Option<PathBuf>,
#[arg(short, long)]
user: Option<String>,
chroot: PathBuf,
#[arg(trailing_var_arg = true)]
command: Vec<OsString>,
}
impl ExecParams {
fn exec(&self) -> Result<(), Error> {
let ExecParams {
env,
env_file,
workdir,
user,
chroot,
command,
..
} = self;
let Some(([command], args)) = command.split_at_checked(1) else {
return Err(Error::new(
eyre!("command cannot be empty"),
ErrorKind::InvalidRequest,
));
};
let mut cmd = StdCommand::new(command);
let passwd = std::fs::read_to_string(chroot.join("etc/passwd"))
.with_ctx(|_| (ErrorKind::Filesystem, "read /etc/passwd"))
.log_err()
.unwrap_or_default();
let mut home = None;
if let Some((uid, gid)) =
if let Some(uid) = user.as_deref().and_then(|u| u.parse::<u32>().ok()) {
Some((uid, uid))
} else if let Some((uid, gid)) = user
.as_deref()
.and_then(|u| u.split_once(":"))
.and_then(|(u, g)| Some((u.parse::<u32>().ok()?, g.parse::<u32>().ok()?)))
{
Some((uid, gid))
} else if let Some(user) = user {
Some(
if let Some((uid, gid)) = passwd.lines().find_map(|l| {
let l = l.trim();
let mut split = l.split(":");
if user != split.next()? {
return None;
}
split.next(); // throw away x
let uid = split.next()?.parse().ok()?;
let gid = split.next()?.parse().ok()?;
split.next(); // throw away group name
home = split.next();
Some((uid, gid))
// uid gid
}) {
(uid, gid)
} else if user == "root" {
(0, 0)
} else {
None.or_not_found(lazy_format!("{user} in /etc/passwd"))?
},
)
} else {
None
}
{
if home.is_none() {
home = passwd.lines().find_map(|l| {
let l = l.trim();
let mut split = l.split(":");
split.next(); // throw away user name
split.next(); // throw away x
if split.next()?.parse::<u32>().ok()? != uid {
return None;
}
split.next(); // throw away gid
split.next(); // throw away group name
split.next()
})
};
std::os::unix::fs::chown("/proc/self/fd/0", Some(uid), Some(gid)).ok();
std::os::unix::fs::chown("/proc/self/fd/1", Some(uid), Some(gid)).ok();
std::os::unix::fs::chown("/proc/self/fd/2", Some(uid), Some(gid)).ok();
cmd.uid(uid);
cmd.gid(gid);
} else {
home = Some("/root");
}
cmd.env("HOME", home.unwrap_or("/"));
let env_string = if let Some(env_file) = &env_file {
std::fs::read_to_string(env_file)
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("read {env:?}")))?
} else {
Default::default()
};
let env = env_string
.lines()
.chain(env.iter().map(|l| l.as_str()))
.map(|l| l.trim())
.filter_map(|l| l.split_once("="))
.collect::<BTreeMap<_, _>>();
std::os::unix::fs::chroot(chroot)
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("chroot {chroot:?}")))?;
cmd.args(args);
for (k, v) in env {
cmd.env(k, v);
}
if let Some(workdir) = workdir {
cmd.current_dir(workdir);
} else {
cmd.current_dir("/");
}
Err(cmd.exec().into())
}
}
pub fn launch(
_: ContainerCliContext,
ExecParams {
force_tty,
force_stderr_tty,
pty_size,
env,
env_file,
workdir,
user,
chroot,
command,
}: ExecParams,
) -> Result<(), Error> {
use std::io::Write;
kill_init(Path::new("/proc"), &chroot)?;
let mut sig = signal_hook::iterator::Signals::new(FWD_SIGNALS)?;
let (send_pid, recv_pid) = oneshot::channel();
std::thread::spawn(move || {
if let Ok(pid) = recv_pid.blocking_recv() {
for sig in sig.forever() {
match nix::sys::signal::kill(
Pid::from_raw(pid),
Some(nix::sys::signal::Signal::try_from(sig).unwrap()),
) {
Err(Errno::ESRCH) => Ok(()),
a => a,
}
.unwrap()
}
}
});
let mut stdin = std::io::stdin();
let stdout = std::io::stdout();
let stderr = std::io::stderr();
let stderr_tty = force_stderr_tty || stderr.is_terminal();
let tty = force_tty || (stdin.is_terminal() && stdout.is_terminal());
let raw = if stdin.is_terminal() && stdout.is_terminal() {
Some(termion::get_tty()?.into_raw_mode()?)
} else {
None
};
let (stdin_send, stdin_recv) = oneshot::channel::<Box<dyn Write + Send>>();
std::thread::spawn(move || {
if let Ok(mut cstdin) = stdin_recv.blocking_recv() {
if tty {
let mut buf = [0_u8; CAP_1_KiB];
while let Ok(n) = stdin.read(&mut buf) {
if n == 0 {
break;
}
cstdin.write_all(&buf[..n]).ok();
cstdin.flush().ok();
}
} else {
std::io::copy(&mut stdin, &mut cstdin).unwrap();
}
}
});
let (stdout_send, stdout_recv) = oneshot::channel::<Box<dyn std::io::Read + Send>>();
let stdout_thread = std::thread::spawn(move || {
if let Ok(mut cstdout) = stdout_recv.blocking_recv() {
if tty {
let mut stdout = stdout.lock();
let mut buf = [0_u8; CAP_1_KiB];
while let Ok(n) = cstdout.read(&mut buf) {
if n == 0 {
break;
}
stdout.write_all(&buf[..n]).ok();
stdout.flush().ok();
}
} else {
std::io::copy(&mut cstdout, &mut stdout.lock()).unwrap();
}
}
});
let (stderr_send, stderr_recv) = oneshot::channel::<Box<dyn std::io::Read + Send>>();
let stderr_thread = if !stderr_tty {
Some(std::thread::spawn(move || {
if let Ok(mut cstderr) = stderr_recv.blocking_recv() {
std::io::copy(&mut cstderr, &mut stderr.lock()).unwrap();
}
}))
} else {
None
};
nix::sched::unshare(CloneFlags::CLONE_NEWPID)
.with_ctx(|_| (ErrorKind::Filesystem, "unshare pid ns"))?;
nix::sched::unshare(CloneFlags::CLONE_NEWCGROUP)
.with_ctx(|_| (ErrorKind::Filesystem, "unshare cgroup ns"))?;
nix::sched::unshare(CloneFlags::CLONE_NEWIPC)
.with_ctx(|_| (ErrorKind::Filesystem, "unshare ipc ns"))?;
if tty {
use pty_process::blocking as pty_process;
let (pty, pts) = pty_process::open().with_kind(ErrorKind::Filesystem)?;
let mut cmd = pty_process::Command::new("/usr/bin/start-container");
cmd = cmd.arg("subcontainer").arg("launch-init");
for env in env {
cmd = cmd.arg("-e").arg(env)
}
if let Some(env_file) = env_file {
cmd = cmd.arg("--env-file").arg(env_file);
}
if let Some(workdir) = workdir {
cmd = cmd.arg("--workdir").arg(workdir);
}
if let Some(user) = user {
cmd = cmd.arg("--user").arg(user);
}
cmd = cmd.arg(&chroot).args(&command);
if !stderr_tty {
cmd = cmd.stderr(Stdio::piped());
}
let mut child = cmd
.spawn(pts)
.map_err(color_eyre::eyre::Report::msg)
.with_ctx(|_| (ErrorKind::Filesystem, "spawning child process"))?;
send_pid.send(child.id() as i32).unwrap_or_default();
if let Some(pty_size) = pty_size.or_else(|| TermSize::get_current()) {
let size = if let Some((x, y)) = pty_size.pixels {
::pty_process::Size::new_with_pixel(pty_size.rows, pty_size.cols, x, y)
} else {
::pty_process::Size::new(pty_size.rows, pty_size.cols)
};
pty.resize(size).with_kind(ErrorKind::Filesystem)?;
}
let shared = ArcPty(Arc::new(pty));
stdin_send
.send(Box::new(shared.clone()))
.unwrap_or_default();
stdout_send
.send(Box::new(shared.clone()))
.unwrap_or_default();
if let Some(stderr) = child.stderr.take() {
stderr_send.send(Box::new(stderr)).unwrap_or_default();
}
let exit = child
.wait()
.with_ctx(|_| (ErrorKind::Filesystem, "waiting on child process"))?;
stdout_thread.join().unwrap();
stderr_thread.map(|t| t.join().unwrap());
if let Some(code) = exit.code() {
drop(raw);
std::process::exit(code);
} else if exit.success() || exit.signal() == Some(15) {
Ok(())
} else {
Err(Error::new(
color_eyre::eyre::Report::msg(exit),
ErrorKind::Unknown,
))
}
} else {
let mut cmd = StdCommand::new("/usr/bin/start-container");
cmd.arg("subcontainer").arg("launch-init");
for env in env {
cmd.arg("-e").arg(env);
}
if let Some(env_file) = env_file {
cmd.arg("--env-file").arg(env_file);
}
if let Some(workdir) = workdir {
cmd.arg("--workdir").arg(workdir);
}
if let Some(user) = user {
cmd.arg("--user").arg(user);
}
cmd.arg(&chroot);
cmd.args(&command);
cmd.stdin(Stdio::piped());
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let mut child = cmd
.spawn()
.map_err(color_eyre::eyre::Report::msg)
.with_ctx(|_| (ErrorKind::Filesystem, "spawning child process"))?;
send_pid.send(child.id() as i32).unwrap_or_default();
stdin_send
.send(Box::new(child.stdin.take().unwrap()))
.unwrap_or_default();
stdout_send
.send(Box::new(child.stdout.take().unwrap()))
.unwrap_or_default();
stderr_send
.send(Box::new(child.stderr.take().unwrap()))
.unwrap_or_default();
// TODO: subreaping, signal handling
let exit = child
.wait()
.with_ctx(|_| (ErrorKind::Filesystem, "waiting on child process"))?;
stdout_thread.join().unwrap();
stderr_thread.map(|t| t.join().unwrap());
if let Some(code) = exit.code() {
nix::mount::umount(&chroot.join("proc"))
.with_ctx(|_| (ErrorKind::Filesystem, "umount procfs"))?;
std::process::exit(code);
} else if exit.success() || exit.signal() == Some(15) {
Ok(())
} else {
Err(Error::new(
color_eyre::eyre::Report::msg(exit),
ErrorKind::Unknown,
))
}
}
}
pub fn launch_init(_: ContainerCliContext, params: ExecParams) -> Result<(), Error> {
nix::mount::mount(
Some("proc"),
&params.chroot.join("proc"),
Some("proc"),
nix::mount::MsFlags::empty(),
None::<&str>,
)
.with_ctx(|_| (ErrorKind::Filesystem, "mount procfs"))?;
if params.command.is_empty() {
signal_hook::iterator::Signals::new(signal_hook::consts::TERM_SIGNALS)?
.forever()
.next();
std::process::exit(0)
} else {
params.exec()
}
}
#[derive(Clone)]
struct ArcPty(Arc<pty_process::blocking::Pty>);
impl std::io::Write for ArcPty {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
(&*self.0).write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
(&*self.0).flush()
}
}
impl std::io::Read for ArcPty {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
(&*self.0).read(buf)
}
}
pub fn exec(
_: ContainerCliContext,
ExecParams {
force_tty,
force_stderr_tty,
pty_size,
env,
env_file,
workdir,
user,
chroot,
command,
}: ExecParams,
) -> Result<(), Error> {
use std::io::Write;
let mut sig = signal_hook::iterator::Signals::new(FWD_SIGNALS)?;
let (send_pid, recv_pid) = oneshot::channel();
std::thread::spawn(move || {
if let Ok(pid) = recv_pid.blocking_recv() {
for sig in sig.forever() {
match nix::sys::signal::kill(
Pid::from_raw(pid),
Some(nix::sys::signal::Signal::try_from(sig).unwrap()),
) {
Err(Errno::ESRCH) => Ok(()),
a => a,
}
.unwrap();
}
}
});
let mut stdin = std::io::stdin();
let stdout = std::io::stdout();
let stderr = std::io::stderr();
let stderr_tty = force_stderr_tty || stderr.is_terminal();
let tty = force_tty || (stdin.is_terminal() && stdout.is_terminal());
let raw = if stdin.is_terminal() && stdout.is_terminal() {
Some(termion::get_tty()?.into_raw_mode()?)
} else {
None
};
let (stdin_send, stdin_recv) = oneshot::channel::<Box<dyn Write + Send>>();
std::thread::spawn(move || {
if let Ok(mut cstdin) = stdin_recv.blocking_recv() {
if tty {
let mut buf = [0_u8; CAP_1_KiB];
while let Ok(n) = stdin.read(&mut buf) {
if n == 0 {
break;
}
cstdin.write_all(&buf[..n]).ok();
cstdin.flush().ok();
}
} else {
std::io::copy(&mut stdin, &mut cstdin).unwrap();
}
}
});
let (stdout_send, stdout_recv) = oneshot::channel::<Box<dyn std::io::Read + Send>>();
let stdout_thread = std::thread::spawn(move || {
if let Ok(mut cstdout) = stdout_recv.blocking_recv() {
if tty {
let mut stdout = stdout.lock();
let mut buf = [0_u8; CAP_1_KiB];
while let Ok(n) = cstdout.read(&mut buf) {
if n == 0 {
break;
}
stdout.write_all(&buf[..n]).ok();
stdout.flush().ok();
}
} else {
std::io::copy(&mut cstdout, &mut stdout.lock()).unwrap();
}
}
});
let (stderr_send, stderr_recv) = oneshot::channel::<Box<dyn std::io::Read + Send>>();
let stderr_thread = if !stderr_tty {
Some(std::thread::spawn(move || {
if let Ok(mut cstderr) = stderr_recv.blocking_recv() {
std::io::copy(&mut cstderr, &mut stderr.lock()).unwrap();
}
}))
} else {
None
};
nix::sched::setns(
open_file_read(chroot.join("proc/1/ns/pid"))?,
CloneFlags::CLONE_NEWPID,
)
.with_ctx(|_| (ErrorKind::Filesystem, "set pid ns"))?;
nix::sched::setns(
open_file_read(chroot.join("proc/1/ns/cgroup"))?,
CloneFlags::CLONE_NEWCGROUP,
)
.with_ctx(|_| (ErrorKind::Filesystem, "set cgroup ns"))?;
nix::sched::setns(
open_file_read(chroot.join("proc/1/ns/ipc"))?,
CloneFlags::CLONE_NEWIPC,
)
.with_ctx(|_| (ErrorKind::Filesystem, "set ipc ns"))?;
if tty {
use pty_process::blocking as pty_process;
let (pty, pts) = pty_process::open().with_kind(ErrorKind::Filesystem)?;
let mut cmd = pty_process::Command::new("/usr/bin/start-container");
cmd = cmd.arg("subcontainer").arg("exec-command");
for env in env {
cmd = cmd.arg("-e").arg(env);
}
if let Some(env_file) = env_file {
cmd = cmd.arg("--env-file").arg(env_file);
}
if let Some(workdir) = workdir {
cmd = cmd.arg("--workdir").arg(workdir);
}
if let Some(user) = user {
cmd = cmd.arg("--user").arg(user);
}
cmd = cmd.arg(&chroot).args(&command);
if !stderr_tty {
cmd = cmd.stderr(Stdio::piped());
}
let mut child = cmd
.spawn(pts)
.map_err(color_eyre::eyre::Report::msg)
.with_ctx(|_| (ErrorKind::Filesystem, "spawning child process"))?;
send_pid.send(child.id() as i32).unwrap_or_default();
if let Some(pty_size) = pty_size.or_else(|| TermSize::get_current()) {
let size = if let Some((x, y)) = pty_size.pixels {
::pty_process::Size::new_with_pixel(pty_size.rows, pty_size.cols, x, y)
} else {
::pty_process::Size::new(pty_size.rows, pty_size.cols)
};
pty.resize(size).with_kind(ErrorKind::Filesystem)?;
}
let shared = ArcPty(Arc::new(pty));
stdin_send
.send(Box::new(shared.clone()))
.unwrap_or_default();
stdout_send
.send(Box::new(shared.clone()))
.unwrap_or_default();
if let Some(stderr) = child.stderr.take() {
stderr_send.send(Box::new(stderr)).unwrap_or_default();
}
let exit = child
.wait()
.with_ctx(|_| (ErrorKind::Filesystem, "waiting on child process"))?;
stdout_thread.join().unwrap();
stderr_thread.map(|t| t.join().unwrap());
if let Some(code) = exit.code() {
drop(raw);
std::process::exit(code);
} else if exit.success() {
Ok(())
} else {
Err(Error::new(
color_eyre::eyre::Report::msg(exit),
ErrorKind::Unknown,
))
}
} else {
let mut cmd = StdCommand::new("/usr/bin/start-container");
cmd.arg("subcontainer").arg("exec-command");
for env in env {
cmd.arg("-e").arg(env);
}
if let Some(env_file) = env_file {
cmd.arg("--env-file").arg(env_file);
}
if let Some(workdir) = workdir {
cmd.arg("--workdir").arg(workdir);
}
if let Some(user) = user {
cmd.arg("--user").arg(user);
}
cmd.arg(&chroot);
cmd.args(&command);
cmd.stdin(Stdio::piped());
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let mut child = cmd
.spawn()
.map_err(color_eyre::eyre::Report::msg)
.with_ctx(|_| (ErrorKind::Filesystem, "spawning child process"))?;
send_pid.send(child.id() as i32).unwrap_or_default();
stdin_send
.send(Box::new(child.stdin.take().unwrap()))
.unwrap_or_default();
stdout_send
.send(Box::new(child.stdout.take().unwrap()))
.unwrap_or_default();
stderr_send
.send(Box::new(child.stderr.take().unwrap()))
.unwrap_or_default();
let exit = child
.wait()
.with_ctx(|_| (ErrorKind::Filesystem, "waiting on child process"))?;
stdout_thread.join().unwrap();
stderr_thread.map(|t| t.join().unwrap());
if let Some(code) = exit.code() {
std::process::exit(code);
} else if exit.success() {
Ok(())
} else {
Err(Error::new(
color_eyre::eyre::Report::msg(exit),
ErrorKind::Unknown,
))
}
}
}
pub fn exec_command(_: ContainerCliContext, params: ExecParams) -> Result<(), Error> {
params.exec()
}