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