pg_embedded_setup_unpriv/bootstrap/
mod.rs

1//! Bootstraps embedded `PostgreSQL` while adapting to the caller's privileges.
2//!
3//! Provides [`bootstrap_for_tests`] so suites can retrieve structured settings and
4//! prepared environment variables without reimplementing bootstrap orchestration.
5mod env;
6mod mode;
7mod prepare;
8
9use std::time::Duration;
10
11use color_eyre::eyre::Context;
12use postgresql_embedded::Settings;
13
14use crate::{
15    PgEnvCfg,
16    error::{BootstrapResult, Result as CrateResult},
17};
18
19pub use env::TestBootstrapEnvironment;
20pub use mode::{ExecutionMode, ExecutionPrivileges, detect_execution_privileges};
21
22use self::{
23    env::{shutdown_timeout_from_env, worker_binary_from_env},
24    mode::determine_execution_mode,
25    prepare::prepare_bootstrap,
26};
27
28const DEFAULT_SETUP_TIMEOUT: Duration = Duration::from_secs(180);
29const DEFAULT_START_TIMEOUT: Duration = Duration::from_secs(60);
30
31/// Structured settings returned from [`bootstrap_for_tests`].
32#[derive(Debug, Clone)]
33pub struct TestBootstrapSettings {
34    /// Privilege level detected for the current process.
35    pub privileges: ExecutionPrivileges,
36    /// Strategy for executing `PostgreSQL` lifecycle commands.
37    pub execution_mode: ExecutionMode,
38    /// `PostgreSQL` configuration prepared for the embedded instance.
39    pub settings: Settings,
40    /// Environment variables required to exercise the embedded instance.
41    pub environment: TestBootstrapEnvironment,
42    /// Optional path to the helper binary used for subprocess execution.
43    pub worker_binary: Option<camino::Utf8PathBuf>,
44    /// Maximum time to allow the worker to complete the setup phase.
45    pub setup_timeout: Duration,
46    /// Maximum time to allow the worker to complete the start phase.
47    pub start_timeout: Duration,
48    /// Grace period granted to `PostgreSQL` during drop before teardown proceeds regardless.
49    pub shutdown_timeout: Duration,
50}
51
52/// Bootstraps an embedded `PostgreSQL` instance, branching between root and unprivileged flows.
53///
54/// The bootstrap honours the following environment variables when present:
55/// - `PG_RUNTIME_DIR`: Overrides the `PostgreSQL` installation directory.
56/// - `PG_DATA_DIR`: Overrides the data directory used for initialisation.
57/// - `PG_SUPERUSER`: Sets the superuser account name.
58/// - `PG_PASSWORD`: Supplies the superuser password.
59///
60/// When executed as `root` on Unix platforms the runtime drops privileges to the `nobody` user
61/// and prepares the filesystem on that user's behalf. Unprivileged executions reuse the current
62/// user identity. The function returns a [`crate::Error`] describing failures encountered during
63/// bootstrap.
64///
65/// This convenience wrapper discards the detailed [`TestBootstrapSettings`]. Call
66/// [`bootstrap_for_tests`] to obtain the structured response for assertions.
67///
68/// # Examples
69/// ```rust
70/// use pg_embedded_setup_unpriv::run;
71///
72/// fn main() -> Result<(), pg_embedded_setup_unpriv::Error> {
73///     run()?;
74///     Ok(())
75/// }
76/// ```
77///
78/// # Errors
79/// Returns an error when bootstrap preparation fails or when subprocess orchestration
80/// cannot be configured.
81pub fn run() -> CrateResult<()> {
82    orchestrate_bootstrap()?;
83    Ok(())
84}
85
86/// Bootstraps `PostgreSQL` for integration tests and surfaces the prepared settings.
87///
88/// # Examples
89/// ```no_run
90/// use pg_embedded_setup_unpriv::bootstrap_for_tests;
91/// use temp_env::with_vars;
92///
93/// # fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
94/// let bootstrap = bootstrap_for_tests()?;
95/// with_vars(bootstrap.environment.to_env(), || -> pg_embedded_setup_unpriv::BootstrapResult<()> {
96///     // Launch application logic that relies on `bootstrap.settings` here.
97///     Ok(())
98/// })?;
99/// # Ok(())
100/// # }
101/// ```
102///
103/// # Errors
104/// Returns an error when bootstrap preparation fails or when subprocess orchestration
105/// cannot be configured.
106pub fn bootstrap_for_tests() -> BootstrapResult<TestBootstrapSettings> {
107    orchestrate_bootstrap()
108}
109
110fn orchestrate_bootstrap() -> BootstrapResult<TestBootstrapSettings> {
111    if let Err(err) = color_eyre::install() {
112        tracing::debug!("color_eyre already installed: {err}");
113    }
114
115    let privileges = detect_execution_privileges();
116    let cfg = PgEnvCfg::load().context("failed to load configuration via OrthoConfig")?;
117    let settings = cfg.to_settings()?;
118    let worker_binary = worker_binary_from_env()?;
119    let execution_mode = determine_execution_mode(privileges, worker_binary.as_ref())?;
120    let shutdown_timeout = shutdown_timeout_from_env()?;
121    let prepared = prepare_bootstrap(privileges, settings, &cfg)?;
122
123    Ok(TestBootstrapSettings {
124        privileges,
125        execution_mode,
126        settings: prepared.settings,
127        environment: prepared.environment,
128        worker_binary,
129        setup_timeout: DEFAULT_SETUP_TIMEOUT,
130        start_timeout: DEFAULT_START_TIMEOUT,
131        shutdown_timeout,
132    })
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use camino::Utf8PathBuf;
139    use temp_env::with_vars;
140    use tempfile::tempdir;
141
142    #[test]
143    fn orchestrate_bootstrap_respects_env_overrides() {
144        if detect_execution_privileges() == ExecutionPrivileges::Root {
145            tracing::warn!(
146                "skipping orchestrate test because root privileges require PG_EMBEDDED_WORKER"
147            );
148            return;
149        }
150
151        let runtime = tempdir().expect("runtime dir");
152        let data = tempdir().expect("data dir");
153        let runtime_path =
154            Utf8PathBuf::from_path_buf(runtime.path().to_path_buf()).expect("runtime dir utf8");
155        let data_path =
156            Utf8PathBuf::from_path_buf(data.path().to_path_buf()).expect("data dir utf8");
157
158        let settings = with_vars(
159            [
160                ("PG_RUNTIME_DIR", Some(runtime_path.as_str())),
161                ("PG_DATA_DIR", Some(data_path.as_str())),
162                ("PG_SUPERUSER", Some("bootstrap_test")),
163                ("PG_PASSWORD", Some("bootstrap_test_pw")),
164                ("PG_EMBEDDED_WORKER", None),
165            ],
166            || orchestrate_bootstrap().expect("bootstrap to succeed"),
167        );
168
169        assert_paths(&settings, &runtime_path, &data_path);
170        assert_identity(&settings, "bootstrap_test", "bootstrap_test_pw");
171        assert_environment(&settings, &runtime_path);
172    }
173
174    #[test]
175    fn run_succeeds_with_customised_paths() {
176        if detect_execution_privileges() == ExecutionPrivileges::Root {
177            tracing::warn!("skipping run test because root privileges require PG_EMBEDDED_WORKER");
178            return;
179        }
180
181        let runtime = tempdir().expect("runtime dir");
182        let data = tempdir().expect("data dir");
183        let runtime_path =
184            Utf8PathBuf::from_path_buf(runtime.path().to_path_buf()).expect("runtime dir utf8");
185        let data_path =
186            Utf8PathBuf::from_path_buf(data.path().to_path_buf()).expect("data dir utf8");
187
188        with_vars(
189            [
190                ("PG_RUNTIME_DIR", Some(runtime_path.as_str())),
191                ("PG_DATA_DIR", Some(data_path.as_str())),
192                ("PG_SUPERUSER", Some("bootstrap_run")),
193                ("PG_PASSWORD", Some("bootstrap_run_pw")),
194                ("PG_EMBEDDED_WORKER", None),
195            ],
196            || {
197                run().expect("run should bootstrap successfully");
198
199                let cache_dir = runtime_path.join("cache");
200                let run_dir = runtime_path.join("run");
201                assert!(cache_dir.exists(), "cache directory should be created");
202                assert!(run_dir.exists(), "runtime directory should be created");
203            },
204        );
205    }
206
207    fn assert_paths(
208        settings: &TestBootstrapSettings,
209        runtime_path: &Utf8PathBuf,
210        data_path: &Utf8PathBuf,
211    ) {
212        let observed_install =
213            Utf8PathBuf::from_path_buf(settings.settings.installation_dir.clone())
214                .expect("installation dir utf8");
215        let observed_data =
216            Utf8PathBuf::from_path_buf(settings.settings.data_dir.clone()).expect("data dir utf8");
217
218        assert_eq!(observed_install.as_path(), runtime_path.as_path());
219        assert_eq!(observed_data.as_path(), data_path.as_path());
220    }
221
222    fn assert_identity(
223        settings: &TestBootstrapSettings,
224        expected_user: &str,
225        expected_password: &str,
226    ) {
227        assert_eq!(settings.settings.username, expected_user);
228        assert_eq!(settings.settings.password, expected_password);
229        assert_eq!(settings.privileges, ExecutionPrivileges::Unprivileged);
230        assert_eq!(settings.execution_mode, ExecutionMode::InProcess);
231        assert!(settings.worker_binary.is_none());
232    }
233
234    fn assert_environment(settings: &TestBootstrapSettings, runtime_path: &Utf8PathBuf) {
235        let env_pairs = settings.environment.to_env();
236        let pgpass = runtime_path.join(".pgpass");
237        assert!(env_pairs.contains(&("PGPASSFILE".into(), Some(pgpass.as_str().into()))));
238        assert_eq!(settings.environment.home.as_path(), runtime_path.as_path());
239    }
240}