use std::net::TcpListener;
use camino::{Utf8Path, Utf8PathBuf};
use color_eyre::eyre::{Context, eyre};
use nix::unistd::User;
use postgresql_embedded::Settings;
use super::{
PreparedBootstrap,
ensure_parents_for_paths,
log_sanitized_settings,
prepare_xdg_dirs,
resolve_settings_paths_for_uid,
unix_user::{ensure_install_dir_for_user, ensure_pgpass_for_user},
};
use crate::{
PgEnvCfg,
bootstrap::env::{TestBootstrapEnvironment, XdgDirs, prepare_timezone_env},
error::{BootstrapError, BootstrapResult},
privileges::{ensure_dir_for_user, ensure_tree_owned_by_user, make_data_dir_private},
};
pub(super) fn bootstrap_with_root(
mut settings: Settings,
cfg: &PgEnvCfg,
layout: super::DataLayout,
) -> BootstrapResult<PreparedBootstrap> {
settings.temporary = false;
ensure_root_port(&mut settings)?;
let nobody_user = User::from_name("nobody")
.context("failed to resolve user 'nobody'")?
.ok_or_else(|| color_eyre::eyre::eyre!("user 'nobody' not found"))?;
let mut paths = resolve_settings_paths_for_uid(&mut settings, cfg, nobody_user.uid)?;
let has_slot = super::claim_derived_slot(&mut settings, &mut paths, layout)?;
super::reuse_existing_password(
&mut settings,
&paths.data_dir,
&paths.password_file,
cfg.password.is_some(),
)?;
log_sanitized_settings(&settings);
ensure_parents_for_paths(&paths, |path| ensure_parent_for_user(path, &nobody_user))?;
ensure_install_dir_for_user(&paths.install_dir, &nobody_user)?;
make_data_dir_private(&paths.data_dir, &nobody_user)?;
let timezone = prepare_timezone_env()?;
let xdg = prepare_xdg_dirs(&paths.install_dir)?;
ensure_xdg_dirs_owned_by_user(&xdg, &nobody_user)?;
super::stale_password::discard_orphaned_password_file(&paths.data_dir, &paths.password_file)?;
ensure_pgpass_for_user(&paths.password_file, &nobody_user)?;
if has_slot {
ensure_slot_lock_owned_by_user(&paths.data_dir, &nobody_user)?;
}
ensure_tree_owned_by_user(&paths.install_dir, &nobody_user)?;
if paths.data_default {
ensure_tree_owned_by_user(&paths.data_dir, &nobody_user)?;
}
let environment = TestBootstrapEnvironment::from_components(xdg, paths.password_file, timezone);
Ok(PreparedBootstrap {
settings,
environment,
})
}
fn ensure_slot_lock_owned_by_user(data_dir: &Utf8Path, user: &User) -> BootstrapResult<()> {
let lock = super::cluster_slot::lock_path(data_dir);
nix::unistd::chown(lock.as_std_path(), Some(user.uid), Some(user.gid))
.with_context(|| format!("failed to hand the slot lock {lock} to {}", user.name))
.map_err(BootstrapError::from)
}
fn ensure_root_port(settings: &mut Settings) -> BootstrapResult<()> {
if settings.port > 0 {
return Ok(());
}
let host = root_bind_host(settings);
let listener = TcpListener::bind((host, 0))
.map_err(|err| BootstrapError::from(eyre!("failed to allocate port: {err}")))?;
let port = listener
.local_addr()
.map_err(|err| BootstrapError::from(eyre!("failed to read allocated port: {err}")))?
.port();
settings.port = port;
Ok(())
}
fn root_bind_host(settings: &Settings) -> &str {
let host = settings.host.as_str();
if host.is_empty() || host.starts_with('/') {
"127.0.0.1"
} else {
host
}
}
pub(super) fn ensure_xdg_dirs_owned_by_user(xdg: &XdgDirs, user: &User) -> BootstrapResult<()> {
ensure_dir_for_user(&xdg.cache, user, 0o755)?;
ensure_dir_for_user(&xdg.runtime, user, 0o700)?;
Ok(())
}
pub(super) fn ensure_parent_for_user(path: &Utf8PathBuf, user: &User) -> BootstrapResult<()> {
if let Some(parent) = path.parent() {
ensure_dir_for_user(parent, user, 0o755)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::{ffi::CString, os::unix::fs::MetadataExt, path::PathBuf};
use nix::unistd::{getegid, geteuid};
use rstest::rstest;
use super::*;
fn current_user() -> User {
User {
name: String::from("pg-embed-test"),
passwd: CString::default(),
uid: geteuid(),
gid: getegid(),
#[cfg(not(all(target_os = "android", target_pointer_width = "32")))]
gecos: CString::default(),
dir: PathBuf::from("/nonexistent"),
shell: PathBuf::from("/nonexistent"),
#[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "dragonfly"))]
class: CString::default(),
#[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "dragonfly"))]
change: 0,
#[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "dragonfly"))]
expire: 0,
}
}
#[test]
fn a_slot_lock_is_handed_to_the_cluster_user() {
let temp = tempfile::tempdir().expect("tempdir");
let data_dir = camino::Utf8PathBuf::from_path_buf(temp.path().join("1-1-0")).expect("utf8");
let lock = super::super::cluster_slot::lock_path(&data_dir);
std::fs::write(&lock, b"").expect("lock file");
let user = current_user();
ensure_slot_lock_owned_by_user(&data_dir, &user).expect("hand over the lock");
let metadata = std::fs::metadata(lock.as_std_path()).expect("lock metadata");
assert_eq!(metadata.uid(), user.uid.as_raw());
assert_eq!(metadata.gid(), user.gid.as_raw());
}
#[test]
fn a_slot_lock_is_handed_to_nobody_when_root() {
if !geteuid().is_root() {
return;
}
let nobody = User::from_name("nobody")
.expect("look up nobody")
.expect("nobody exists");
let temp = tempfile::tempdir().expect("tempdir");
let data_dir = camino::Utf8PathBuf::from_path_buf(temp.path().join("1-1-0")).expect("utf8");
let lock = super::super::cluster_slot::lock_path(&data_dir);
std::fs::write(&lock, b"").expect("lock file");
ensure_slot_lock_owned_by_user(&data_dir, &nobody).expect("hand over the lock");
let metadata = std::fs::metadata(lock.as_std_path()).expect("lock metadata");
assert_eq!(metadata.uid(), nobody.uid.as_raw());
}
#[test]
fn bootstrap_with_root_reuses_stored_password() {
if !geteuid().is_root() {
return; }
let temp = tempfile::tempdir().expect("tempdir");
let base = camino::Utf8PathBuf::from_path_buf(temp.path().to_path_buf()).expect("utf8");
let install = base.join("install");
let data = base.join("data");
std::fs::create_dir_all(&install).expect("install dir");
std::fs::create_dir_all(&data).expect("data dir");
std::fs::write(data.join("PG_VERSION"), "17\n").expect("marker");
std::fs::write(install.join(".pgpass"), "stored-secret\n").expect("password file");
let cfg = PgEnvCfg {
runtime_dir: Some(install),
data_dir: Some(data),
..PgEnvCfg::default()
};
let settings = cfg.to_settings().expect("settings");
let prepared = bootstrap_with_root(settings, &cfg, super::super::DataLayout::PerCluster)
.expect("root bootstrap");
assert_eq!(prepared.settings.password, "stored-secret");
}
#[rstest]
#[case("", "127.0.0.1")]
#[case("/var/run/pg.sock", "127.0.0.1")]
#[case("db.internal", "db.internal")]
fn root_bind_host_falls_back_for_socket_or_empty(#[case] host: &str, #[case] expected: &str) {
let settings = Settings {
host: host.to_owned(),
..Settings::default()
};
assert_eq!(root_bind_host(&settings), expected);
}
#[test]
fn ensure_root_port_allocates_when_zero() {
let mut settings = Settings {
host: String::new(),
port: 0,
..Settings::default()
};
ensure_root_port(&mut settings).expect("allocate ephemeral port");
assert!(settings.port > 0, "an ephemeral port should be assigned");
}
#[test]
fn ensure_root_port_keeps_existing_port() {
let mut settings = Settings {
port: 5599,
..Settings::default()
};
ensure_root_port(&mut settings).expect("existing port retained");
assert_eq!(settings.port, 5599);
}
#[test]
fn ensure_parent_for_user_creates_missing_parent() {
let user = current_user();
let temp = tempfile::tempdir().expect("tempdir");
let base = camino::Utf8Path::from_path(temp.path()).expect("utf8 tempdir");
let child = base.join("parent/child");
ensure_parent_for_user(&child, &user).expect("prepare parent directory");
let parent = base.join("parent");
assert!(parent.is_dir(), "the parent directory should be created");
let metadata = std::fs::metadata(parent.as_std_path()).expect("parent directory metadata");
assert_eq!(
metadata.uid(),
user.uid.as_raw(),
"parent directory should be owned by the requested user"
);
assert_eq!(
metadata.gid(),
user.gid.as_raw(),
"parent directory should be group-owned by the requested user"
);
}
}