mod common;
use boxlite::BoxCommand;
use tokio_stream::StreamExt;
async fn exec_stdout(handle: &boxlite::LiteBox, cmd: BoxCommand) -> String {
let mut execution = handle.exec(cmd).await.expect("exec failed");
let mut stdout = String::new();
if let Some(mut stream) = execution.stdout() {
while let Some(chunk) = stream.next().await {
stdout.push_str(&chunk);
}
}
let result = execution.wait().await.expect("wait failed");
assert_eq!(result.exit_code, 0, "command should exit 0");
stdout
}
struct TestBox {
handle: boxlite::LiteBox,
runtime: boxlite::BoxliteRuntime,
_home: boxlite_test_utils::home::PerTestBoxHome,
}
impl TestBox {
async fn new() -> Self {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = boxlite::BoxliteRuntime::new(boxlite::runtime::options::BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let handle = runtime.create(common::alpine_opts(), None).await.unwrap();
handle.start().await.unwrap();
Self {
handle,
runtime,
_home: home,
}
}
async fn teardown(self) {
self.handle.stop().await.unwrap();
let _ = self.runtime.remove(self.handle.id().as_str(), true).await;
let _ = self
.runtime
.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT))
.await;
}
}
#[tokio::test]
async fn test_exec_default_user_is_root() {
let tb = TestBox::new().await;
let stdout = exec_stdout(&tb.handle, BoxCommand::new("id").arg("-u")).await;
assert_eq!(stdout.trim(), "0", "default user should be root (uid 0)");
tb.teardown().await;
}
#[tokio::test]
async fn test_exec_user_overrides() {
let tb = TestBox::new().await;
let cases: Vec<(&str, BoxCommand, &str)> = vec![
(
"numeric uid:gid",
BoxCommand::new("id").arg("-u").user("65534:65534"),
"65534",
),
(
"user by name",
BoxCommand::new("id").arg("-un").user("nobody"),
"nobody",
),
(
"uid:gid both set",
BoxCommand::new("sh")
.args(["-c", "echo uid=$(id -u) gid=$(id -g)"])
.user("1000:2000"),
"uid=1000",
),
];
for (desc, cmd, expected) in cases {
let stdout = exec_stdout(&tb.handle, cmd).await;
assert!(
stdout.contains(expected),
"{desc}: expected stdout to contain {expected:?}, got: {stdout:?}"
);
}
let stdout = exec_stdout(
&tb.handle,
BoxCommand::new("sh")
.args(["-c", "echo gid=$(id -g)"])
.user("1000:2000"),
)
.await;
assert!(
stdout.contains("gid=2000"),
"stdout should contain gid=2000, got: {stdout:?}"
);
tb.teardown().await;
}
#[tokio::test]
async fn test_non_root_image_user_exec_uid_and_inode_ownership() {
const IMAGE_UID: u32 = 472;
let host_uid = unsafe { libc::getuid() };
if host_uid == IMAGE_UID {
eprintln!(
"skipping: host uid={host_uid} == image uid={IMAGE_UID}; \
test would be vacuous"
);
return;
}
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = boxlite::BoxliteRuntime::new(boxlite::runtime::options::BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let handle = runtime
.create(
boxlite::runtime::options::BoxOptions {
rootfs: boxlite::runtime::options::RootfsSpec::Image(
"grafana/grafana:latest".into(),
),
auto_delete: Some(0),
..Default::default()
},
None,
)
.await
.expect("create grafana box");
handle.start().await.expect("start grafana box");
let exec_uid = exec_stdout(&handle, BoxCommand::new("id").arg("-u")).await;
assert_eq!(
exec_uid.trim(),
"472",
"exec uid must be 472 (OCI User=\"472\"), got: {exec_uid:?}"
);
let inode_uid = exec_stdout(
&handle,
BoxCommand::new("stat").args(["-c", "%u", "/var/lib/grafana"]),
)
.await;
assert_eq!(
inode_uid.trim(),
"472",
"/var/lib/grafana inode must be uid=472 (image-declared), not uid=0 (root): \
normalize_inodes_with_debugfs stamps ownership at ext4 build time"
);
handle.stop().await.expect("stop");
let _ = runtime.remove(handle.id().as_str(), true).await;
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}