Skip to main content

pg_embedded_setup_unpriv/
lib.rs

1//! Facilitates preparing an embedded `PostgreSQL` instance while dropping root
2//! privileges.
3//!
4//! The library owns the lifecycle for configuring paths, permissions, and
5//! process identity so the bundled `PostgreSQL` binaries can initialise safely
6//! under an unprivileged account.
7
8mod bootstrap;
9pub mod cache;
10mod cleanup_helpers;
11mod cluster;
12mod env;
13mod error;
14mod fs;
15mod observability;
16#[cfg(all(
17    unix,
18    any(
19        target_os = "linux",
20        target_os = "android",
21        target_os = "freebsd",
22        target_os = "openbsd",
23        target_os = "dragonfly",
24    ),
25))]
26mod privileges;
27#[doc(hidden)]
28pub mod test_support;
29#[doc(hidden)]
30pub mod worker;
31pub(crate) mod worker_process;
32
33#[doc(hidden)]
34pub mod worker_process_test_api {
35    //! Integration test shims for worker process orchestration.
36
37    pub use crate::cluster::WorkerOperation;
38    use crate::worker_process;
39    pub use crate::worker_process::WorkerRequestArgs;
40
41    #[cfg(all(
42        unix,
43        any(
44            target_os = "linux",
45            target_os = "android",
46            target_os = "freebsd",
47            target_os = "openbsd",
48            target_os = "dragonfly",
49        ),
50        any(test, doc, feature = "privileged-tests"),
51    ))]
52    use crate::worker_process::PrivilegeDropGuard as InnerPrivilegeDropGuard;
53
54    /// Test-visible wrapper around the internal worker request.
55    ///
56    /// Use this helper when integration tests need to exercise worker process
57    /// orchestration without exposing the internals as part of the public API.
58    pub struct WorkerRequest<'a>(worker_process::WorkerRequest<'a>);
59
60    impl<'a> WorkerRequest<'a> {
61        /// Constructs a worker request for invoking an operation in tests.
62        ///
63        /// # Examples
64        ///
65        /// ```ignore
66        /// # use std::time::Duration;
67        /// # use camino::Utf8Path;
68        /// # use postgresql_embedded::Settings;
69        /// # use pg_embedded_setup_unpriv::{
70        /// #     WorkerOperation,
71        /// #     worker_process_test_api::{WorkerRequest, WorkerRequestArgs},
72        /// # };
73        /// # let worker = Utf8Path::new("/tmp/worker");
74        /// # let settings = Settings::default();
75        /// # let env_vars: Vec<(String, Option<String>)> = Vec::new();
76        /// let args = WorkerRequestArgs {
77        ///     worker,
78        ///     settings: &settings,
79        ///     env_vars: &env_vars,
80        ///     operation: WorkerOperation::Setup,
81        ///     timeout: Duration::from_secs(1),
82        /// };
83        /// let request = WorkerRequest::new(args);
84        /// # let _ = request;
85        /// ```
86        #[must_use]
87        pub const fn new(args: WorkerRequestArgs<'a>) -> Self {
88            Self(worker_process::WorkerRequest::new(args))
89        }
90
91        /// Returns a reference to the wrapped worker request.
92        pub(crate) const fn inner(&self) -> &worker_process::WorkerRequest<'a> {
93            &self.0
94        }
95    }
96
97    /// Executes a worker request whilst returning crate-level errors.
98    pub fn run(request: &WorkerRequest<'_>) -> crate::BootstrapResult<()> {
99        worker_process::run(request.inner())
100    }
101
102    /// Guard that restores the privilege-drop toggle when tests finish.
103    #[cfg(all(
104        unix,
105        any(
106            target_os = "linux",
107            target_os = "android",
108            target_os = "freebsd",
109            target_os = "openbsd",
110            target_os = "dragonfly",
111        ),
112        any(test, doc, feature = "privileged-tests"),
113    ))]
114    pub struct PrivilegeDropGuard {
115        _inner: InnerPrivilegeDropGuard,
116    }
117
118    /// Temporarily disables privilege dropping so tests can run deterministic
119    /// worker binaries without adjusting file ownership.
120    #[cfg(all(
121        unix,
122        any(
123            target_os = "linux",
124            target_os = "android",
125            target_os = "freebsd",
126            target_os = "openbsd",
127            target_os = "dragonfly",
128        ),
129        any(test, doc, feature = "privileged-tests"),
130    ))]
131    #[must_use]
132    pub fn disable_privilege_drop_for_tests() -> PrivilegeDropGuard {
133        PrivilegeDropGuard {
134            _inner: worker_process::disable_privilege_drop_for_tests(),
135        }
136    }
137
138    /// Renders a worker failure for assertion-friendly error strings.
139    #[must_use]
140    pub fn render_failure_for_tests(
141        context: &str,
142        output: &std::process::Output,
143    ) -> crate::BootstrapError {
144        worker_process::render_failure_for_tests(context, output)
145    }
146}
147
148/// Resolves a path to an ambient directory handle paired with the relative path component.
149///
150/// This function provides capability-based filesystem access by opening paths relative to
151/// ambient authority. Absolute paths are opened relative to their parent directory; relative
152/// paths reuse the current working directory.
153///
154/// # Returns
155///
156/// Returns a tuple containing:
157/// - A [`cap_std::fs::Dir`] handle for the parent directory
158/// - A [`camino::Utf8PathBuf`] with the relative component
159///
160/// For absolute paths like `/foo/bar`, returns `(Dir("/foo"), "bar")`.
161/// For relative paths like `baz/qux`, returns `(Dir("."), "baz/qux")`.
162/// For root paths like `/`, returns `(Dir("/"), "")` with an empty relative component.
163///
164/// # Errors
165///
166/// Returns an error if the path cannot be opened as a directory or if path operations fail.
167///
168/// # Examples
169///
170/// ```no_run
171/// use pg_embedded_setup_unpriv::ambient_dir_and_path;
172/// use camino::Utf8Path;
173///
174/// # fn main() -> color_eyre::Result<()> {
175/// let (dir, relative) = ambient_dir_and_path(Utf8Path::new("./data"))?;
176/// // Use dir handle for capability-based operations on relative path
177/// # Ok(())
178/// # }
179/// ```
180pub use crate::fs::ambient_dir_and_path;
181
182#[doc(hidden)]
183pub use crate::env::ScopedEnv;
184pub use bootstrap::{
185    CleanupMode, ExecutionMode, ExecutionPrivileges, TestBootstrapEnvironment,
186    TestBootstrapSettings, bootstrap_for_tests, detect_execution_privileges, find_timezone_dir,
187    run,
188};
189#[cfg(any(doc, test, feature = "cluster-unit-tests", feature = "dev-worker"))]
190#[doc(hidden)]
191pub use cluster::WorkerInvoker;
192#[cfg(any(test, feature = "cluster-unit-tests"))]
193#[doc(hidden)]
194pub use cluster::WorkerOperation;
195pub use cluster::{
196    ClusterGuard, ClusterHandle, ConnectionMetadata, DatabaseName, TemporaryDatabase, TestCluster,
197    TestClusterConnection,
198};
199#[doc(hidden)]
200pub use error::BootstrapResult;
201pub use error::PgEmbeddedError as Error;
202pub use error::{
203    BootstrapError, BootstrapErrorKind, PgEmbeddedError, PrivilegeError, PrivilegeResult, Result,
204};
205#[cfg(feature = "privileged-tests")]
206#[cfg(all(
207    unix,
208    any(
209        target_os = "linux",
210        target_os = "android",
211        target_os = "freebsd",
212        target_os = "openbsd",
213        target_os = "dragonfly",
214    ),
215))]
216#[expect(
217    deprecated,
218    reason = "with_temp_euid() remains exported for backward compatibility whilst deprecated"
219)]
220pub use privileges::with_temp_euid;
221#[cfg(all(
222    unix,
223    any(
224        target_os = "linux",
225        target_os = "android",
226        target_os = "freebsd",
227        target_os = "openbsd",
228        target_os = "dragonfly",
229    ),
230))]
231pub use privileges::{default_paths_for, make_data_dir_private, make_dir_accessible, nobody_uid};
232
233use color_eyre::eyre::{Context, eyre};
234use ortho_config::OrthoConfig;
235use postgresql_embedded::{Settings, VersionReq};
236use serde::{Deserialize, Serialize};
237
238use crate::error::{ConfigError, ConfigResult};
239use camino::Utf8PathBuf;
240use std::ffi::OsString;
241
242/// Captures `PostgreSQL` settings supplied via environment variables.
243#[derive(Debug, Clone, Serialize, Deserialize, OrthoConfig, Default)]
244#[ortho_config(prefix = "PG")]
245///
246/// # Examples
247/// ```
248/// use pg_embedded_setup_unpriv::PgEnvCfg;
249///
250/// let cfg = PgEnvCfg::default();
251/// assert!(cfg.port.is_none());
252/// ```
253pub struct PgEnvCfg {
254    /// Optional semver requirement that constrains the `PostgreSQL` version.
255    pub version_req: Option<String>,
256    /// Port assigned to the embedded `PostgreSQL` server.
257    pub port: Option<u16>,
258    /// Name of the administrative user created for the cluster.
259    pub superuser: Option<String>,
260    /// Password provisioned for the administrative user.
261    pub password: Option<String>,
262    /// Directory used for `PostgreSQL` data files when provided.
263    pub data_dir: Option<Utf8PathBuf>,
264    /// Directory containing the `PostgreSQL` binaries when provided.
265    pub runtime_dir: Option<Utf8PathBuf>,
266    /// Locale applied to `initdb` when specified.
267    pub locale: Option<String>,
268    /// Encoding applied to `initdb` when specified.
269    pub encoding: Option<String>,
270    /// Directory for sharing downloaded `PostgreSQL` binaries across test runs.
271    ///
272    /// When `Some`, this explicit path is used directly by `TestCluster`, bypassing
273    /// the automatic resolution chain. When `None`, the cache directory is resolved
274    /// in the following order:
275    ///
276    /// 1. `PG_BINARY_CACHE_DIR` environment variable (if set and non-empty)
277    /// 2. `$XDG_CACHE_HOME/pg-embedded/binaries` (if `XDG_CACHE_HOME` is set)
278    /// 3. `$HOME/.cache/pg-embedded/binaries` (if `HOME` is set)
279    /// 4. `/tmp/pg-embedded/binaries` (final fallback)
280    pub binary_cache_dir: Option<Utf8PathBuf>,
281}
282
283impl PgEnvCfg {
284    /// Loads configuration from environment variables without parsing CLI arguments.
285    ///
286    /// # Errors
287    /// Returns an error when environment parsing fails or derived configuration
288    /// cannot be represented using UTF-8 paths.
289    pub fn load() -> ConfigResult<Self> {
290        let args = [OsString::from("pg-embedded-setup-unpriv")];
291        Self::load_from_iter(args).map_err(|err| ConfigError::from(eyre!(err)))
292    }
293
294    /// Converts the configuration into a complete `postgresql_embedded::Settings` object.
295    ///
296    /// Applies version, connection, path, and locale settings from the current configuration.
297    /// Returns an error if the version requirement is invalid. This variant does not apply
298    /// test-specific worker limits.
299    ///
300    /// # Examples
301    /// ```no_run
302    /// use pg_embedded_setup_unpriv::PgEnvCfg;
303    ///
304    /// let cfg = PgEnvCfg::default();
305    /// let settings = cfg.to_settings()?;
306    /// # Ok::<(), pg_embedded_setup_unpriv::Error>(())
307    /// ```
308    ///
309    /// # Returns
310    /// A fully configured `Settings` instance on success, or an error if configuration fails.
311    ///
312    /// # Errors
313    /// Returns an error when the semantic version requirement cannot be parsed.
314    pub fn to_settings(&self) -> Result<Settings> {
315        self.to_settings_with_context(false)
316    }
317
318    /// Converts the configuration into `Settings`, applying test-only worker limits.
319    ///
320    /// Use this helper for ephemeral test clusters where resource limits are desirable.
321    ///
322    /// # Examples
323    /// ```no_run
324    /// use pg_embedded_setup_unpriv::PgEnvCfg;
325    ///
326    /// let cfg = PgEnvCfg::default();
327    /// let settings = cfg.to_settings_for_tests()?;
328    /// # Ok::<(), pg_embedded_setup_unpriv::Error>(())
329    /// ```
330    ///
331    /// # Errors
332    /// Returns an error when the semantic version requirement cannot be parsed.
333    pub fn to_settings_for_tests(&self) -> Result<Settings> {
334        self.to_settings_with_context(true)
335    }
336
337    /// Converts the configuration into `Settings`, optionally applying test limits.
338    ///
339    /// Set `for_tests` to `true` to apply the worker limits intended for ephemeral
340    /// test clusters.
341    ///
342    /// # Examples
343    /// ```no_run
344    /// use pg_embedded_setup_unpriv::PgEnvCfg;
345    ///
346    /// let cfg = PgEnvCfg::default();
347    /// let settings = cfg.to_settings_with_context(true)?;
348    /// # Ok::<(), pg_embedded_setup_unpriv::Error>(())
349    /// ```
350    ///
351    /// # Errors
352    /// Returns an error when the semantic version requirement cannot be parsed.
353    pub fn to_settings_with_context(&self, for_tests: bool) -> Result<Settings> {
354        // Disable the internal postgresql_embedded timeout. This crate wraps lifecycle
355        // operations with tokio::time::timeout using setup_timeout/start_timeout from
356        // TestBootstrapSettings, providing consistent timeout behaviour for both
357        // privileged (subprocess) and unprivileged (in-process) execution paths.
358        // The default 5-second timeout is too short for initdb on slower systems.
359        let mut s = Settings {
360            timeout: None,
361            ..Settings::default()
362        };
363
364        self.apply_version(&mut s)?;
365        self.apply_connection(&mut s);
366        self.apply_paths(&mut s);
367        self.apply_locale(&mut s);
368        if for_tests {
369            Self::apply_worker_limits(&mut s);
370        }
371
372        Ok(s)
373    }
374
375    fn apply_version(&self, settings: &mut Settings) -> ConfigResult<()> {
376        if let Some(ref vr) = self.version_req {
377            settings.version =
378                VersionReq::parse(vr).context("PG_VERSION_REQ invalid semver spec")?;
379        }
380        Ok(())
381    }
382
383    fn apply_connection(&self, settings: &mut Settings) {
384        if let Some(p) = self.port {
385            settings.port = p;
386        }
387        if let Some(ref u) = self.superuser {
388            settings.username.clone_from(u);
389        }
390        if let Some(ref pw) = self.password {
391            settings.password.clone_from(pw);
392        }
393    }
394
395    fn apply_paths(&self, settings: &mut Settings) {
396        if let Some(ref dir) = self.data_dir {
397            settings.data_dir = dir.clone().into_std_path_buf();
398        }
399        if let Some(ref dir) = self.runtime_dir {
400            settings.installation_dir = dir.clone().into_std_path_buf();
401        }
402    }
403
404    /// Applies locale and encoding settings to the `PostgreSQL` configuration if specified
405    /// in the environment.
406    ///
407    /// Inserts the `locale` and `encoding` values into the settings configuration map when
408    /// present in the environment configuration.
409    fn apply_locale(&self, settings: &mut Settings) {
410        if let Some(ref loc) = self.locale {
411            settings.configuration.insert("locale".into(), loc.clone());
412        }
413        if let Some(ref enc) = self.encoding {
414            settings
415                .configuration
416                .insert("encoding".into(), enc.clone());
417        }
418    }
419
420    fn apply_worker_limits(settings: &mut Settings) {
421        for (key, value) in WORKER_LIMIT_DEFAULTS {
422            settings
423                .configuration
424                .entry(key.to_owned())
425                .or_insert_with(|| value.to_owned());
426        }
427    }
428}
429
430const WORKER_LIMIT_DEFAULTS: [(&str, &str); 8] = [
431    ("max_connections", "20"),
432    ("max_worker_processes", "2"),
433    ("max_parallel_workers", "0"),
434    ("max_parallel_workers_per_gather", "0"),
435    ("max_parallel_maintenance_workers", "0"),
436    ("autovacuum", "off"),
437    ("max_wal_senders", "0"),
438    ("max_replication_slots", "0"),
439];