fabro/lib/crates/fabro-dev/tests/it/docker_entrypoint.rs
Bryan Helmkamp c224a4517f
fix(docker): grant server access to mounted socket
When the packaged container starts as root, map the mounted Docker socket's group into the container and add the unprivileged fabro user before dropping privileges. This lets Docker sandboxes work with socket mounts from OrbStack, Docker Desktop, and Linux daemons whose socket GID varies by host.
2026-04-27 11:12:44 -07:00

163 lines
4.8 KiB
Rust

use std::os::unix::fs::PermissionsExt as _;
use std::os::unix::net::UnixListener;
use std::path::Path;
use std::process::Command;
use super::{output_text, read_file, workspace_root};
#[test]
fn entrypoint_adds_fabro_to_existing_docker_socket_group() {
let fixture = EntryPointFixture::new();
fixture.write_root_id_command("1000");
fixture.write_socket_gid_command(0);
fixture.write_fake_command("awk", "echo root\n");
fixture.write_default_logging_commands();
let _socket = UnixListener::bind(fixture.socket_path()).expect("binding fake Docker socket");
fixture.run_entrypoint();
let log = fixture.log();
assert!(
log.contains("addgroup fabro root\n"),
"entrypoint should add fabro to the socket's existing group:\n{log}"
);
assert!(
log.contains("su-exec fabro true\n"),
"entrypoint should still drop privileges through su-exec:\n{log}"
);
}
#[test]
fn entrypoint_creates_group_when_docker_socket_gid_is_unknown() {
let fixture = EntryPointFixture::new();
fixture.write_root_id_command("1000");
fixture.write_socket_gid_command(1234);
fixture.write_fake_command("awk", "true\n");
fixture.write_default_logging_commands();
let _socket = UnixListener::bind(fixture.socket_path()).expect("binding fake Docker socket");
fixture.run_entrypoint();
let log = fixture.log();
assert!(
log.contains("addgroup -S -g 1234 docker-sock-1234\n"),
"entrypoint should create a group for an unknown socket gid:\n{log}"
);
assert!(
log.contains("addgroup fabro docker-sock-1234\n"),
"entrypoint should add fabro to the generated socket group:\n{log}"
);
}
struct EntryPointFixture {
root: tempfile::TempDir,
}
impl EntryPointFixture {
fn new() -> Self {
let root = tempfile::tempdir().expect("creating entrypoint test dir");
std::fs::create_dir(root.path().join("bin")).expect("creating fake bin dir");
Self { root }
}
fn socket_path(&self) -> std::path::PathBuf {
self.root.path().join("docker.sock")
}
fn log_path(&self) -> std::path::PathBuf {
self.root.path().join("calls.log")
}
fn bin_path(&self) -> std::path::PathBuf {
self.root.path().join("bin")
}
#[expect(
clippy::disallowed_methods,
reason = "integration test runs the POSIX entrypoint script as a subprocess"
)]
fn run_entrypoint(&self) {
let output = Command::new("/bin/sh")
.arg(workspace_root().join("docker/entrypoint.sh"))
.arg("true")
.env(
"DOCKER_HOST",
format!("unix://{}", self.socket_path().display()),
)
.env("FABRO_HOME", self.root.path().join("home"))
.env("PATH", self.bin_path())
.env("ENTRYPOINT_TEST_LOG", self.log_path())
.output()
.expect("running entrypoint");
assert!(
output.status.success(),
"entrypoint failed\nstdout:\n{}\nstderr:\n{}",
output_text(&output.stdout),
output_text(&output.stderr)
);
}
fn log(&self) -> String {
read_file(self.root.path(), "calls.log")
}
fn write_logging_command(&self, name: &str) {
self.write_fake_command(
name,
r#"printf '%s %s\n' "${0##*/}" "$*" >> "$ENTRYPOINT_TEST_LOG"
"#,
);
}
fn write_default_logging_commands(&self) {
self.write_logging_command("addgroup");
self.write_logging_command("chown");
self.write_logging_command("mkdir");
self.write_logging_command("su-exec");
}
fn write_root_id_command(&self, fabro_groups: &str) {
self.write_fake_command(
"id",
&format!(
r#"case "$*" in
"-u") echo 0 ;;
"-G fabro") echo {fabro_groups} ;;
*) echo "unexpected id args: $*" >&2; exit 64 ;;
esac
"#
),
);
}
fn write_socket_gid_command(&self, gid: u32) {
self.write_fake_command(
"stat",
&format!(
r#"test "$1" = "-c"
test "$2" = "%g"
echo {gid}
"#
),
);
}
fn write_fake_command(&self, name: &str, body: &str) {
let path = self.bin_path().join(name);
write_executable(&path, &format!("#!/bin/sh\nset -eu\n{body}"));
}
}
#[expect(
clippy::disallowed_methods,
reason = "integration test creates executable shell fixtures"
)]
fn write_executable(path: &Path, contents: &str) {
std::fs::write(path, contents).expect("writing fake command");
let mut permissions = std::fs::metadata(path)
.expect("reading fake command metadata")
.permissions();
permissions.set_mode(0o755);
std::fs::set_permissions(path, permissions).expect("marking fake command executable");
}