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;
14pub mod extensions;
15mod fs;
16#[cfg(all(test, feature = "loom-tests"))]
17mod loom_model;
18pub mod observability;
19#[cfg(all(
20 unix,
21 any(
22 target_os = "linux",
23 target_os = "android",
24 target_os = "freebsd",
25 target_os = "openbsd",
26 target_os = "dragonfly",
27 ),
28))]
29mod privileges;
30#[doc(hidden)]
31pub mod test_support;
32#[doc(hidden)]
33pub mod worker;
34pub(crate) mod worker_process;
35
36#[doc(hidden)]
37pub mod worker_process_test_api {
38 //! Integration test shims for worker process orchestration.
39
40 use crate::worker_process;
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 pub use crate::{cluster::WorkerOperation, worker_process::WorkerRequestArgs};
54
55 /// Test-visible wrapper around the internal worker request.
56 ///
57 /// Use this helper when integration tests need to exercise worker process
58 /// orchestration without exposing the internals as part of the public API.
59 pub struct WorkerRequest<'a>(worker_process::WorkerRequest<'a>);
60
61 impl<'a> WorkerRequest<'a> {
62 /// Constructs a worker request for invoking an operation in tests.
63 ///
64 /// # Examples
65 ///
66 /// ```ignore
67 /// # use std::time::Duration;
68 /// # use camino::Utf8Path;
69 /// # use postgresql_embedded::Settings;
70 /// # use pg_embedded_setup_unpriv::{
71 /// # WorkerOperation,
72 /// # worker_process_test_api::{WorkerRequest, WorkerRequestArgs},
73 /// # };
74 /// # let worker = Utf8Path::new("/tmp/worker");
75 /// # let settings = Settings::default();
76 /// # let env_vars: Vec<(String, Option<String>)> = Vec::new();
77 /// let args = WorkerRequestArgs {
78 /// worker,
79 /// settings: &settings,
80 /// env_vars: &env_vars,
81 /// operation: WorkerOperation::Setup,
82 /// timeout: Duration::from_secs(1),
83 /// };
84 /// let request = WorkerRequest::new(args);
85 /// # let _ = request;
86 /// ```
87 #[must_use]
88 pub const fn new(args: WorkerRequestArgs<'a>) -> Self {
89 Self(worker_process::WorkerRequest::new(args))
90 }
91
92 /// Returns a reference to the wrapped worker request.
93 pub(crate) const fn inner(&self) -> &worker_process::WorkerRequest<'a> { &self.0 }
94 }
95
96 /// Executes a worker request whilst returning crate-level errors.
97 pub fn run(request: &WorkerRequest<'_>) -> crate::BootstrapResult<()> {
98 worker_process::run(request.inner())
99 }
100
101 /// Guard that restores the privilege-drop toggle when tests finish.
102 #[cfg(all(
103 unix,
104 any(
105 target_os = "linux",
106 target_os = "android",
107 target_os = "freebsd",
108 target_os = "openbsd",
109 target_os = "dragonfly",
110 ),
111 any(test, doc, feature = "privileged-tests"),
112 ))]
113 pub struct PrivilegeDropGuard {
114 _inner: InnerPrivilegeDropGuard,
115 }
116
117 /// Temporarily disables privilege dropping so tests can run deterministic
118 /// worker binaries without adjusting file ownership.
119 #[cfg(all(
120 unix,
121 any(
122 target_os = "linux",
123 target_os = "android",
124 target_os = "freebsd",
125 target_os = "openbsd",
126 target_os = "dragonfly",
127 ),
128 any(test, doc, feature = "privileged-tests"),
129 ))]
130 #[must_use]
131 pub fn disable_privilege_drop_for_tests() -> PrivilegeDropGuard {
132 PrivilegeDropGuard {
133 _inner: worker_process::disable_privilege_drop_for_tests(),
134 }
135 }
136
137 /// Renders a worker failure for assertion-friendly error strings.
138 #[must_use]
139 pub fn render_failure_for_tests(
140 context: &str,
141 output: &std::process::Output,
142 ) -> crate::BootstrapError {
143 worker_process::render_failure_for_tests(context, output)
144 }
145}
146
147use std::ffi::OsString;
148
149pub use bootstrap::{
150 CleanupMode,
151 ExecutionMode,
152 ExecutionPrivileges,
153 PasswordReuseOutcome,
154 TestBootstrapEnvironment,
155 TestBootstrapSettings,
156 bootstrap_for_tests,
157 default_paths_under,
158 detect_execution_privileges,
159 find_timezone_dir,
160 reuse_existing_password,
161 run,
162 stored_cluster_password,
163};
164use camino::Utf8PathBuf;
165#[cfg(any(doc, test, feature = "cluster-unit-tests", feature = "dev-worker"))]
166#[doc(hidden)]
167pub use cluster::WorkerInvoker;
168#[cfg(any(test, feature = "cluster-unit-tests"))]
169#[doc(hidden)]
170pub use cluster::WorkerOperation;
171pub use cluster::{
172 ClusterGuard,
173 ClusterHandle,
174 ConnectionMetadata,
175 DatabaseName,
176 TemporaryDatabase,
177 TestCluster,
178 TestClusterConnection,
179};
180use color_eyre::eyre::{Context, eyre};
181#[doc(hidden)]
182pub use error::BootstrapResult;
183pub use error::{
184 BootstrapError,
185 BootstrapErrorKind,
186 PgEmbeddedError as Error,
187 PgEmbeddedError,
188 PrivilegeError,
189 PrivilegeResult,
190 Result,
191};
192use ortho_config::OrthoConfig;
193use postgresql_embedded::{Settings, VersionReq};
194#[cfg(feature = "privileged-tests")]
195#[cfg(all(
196 unix,
197 any(
198 target_os = "linux",
199 target_os = "android",
200 target_os = "freebsd",
201 target_os = "openbsd",
202 target_os = "dragonfly",
203 ),
204))]
205#[expect(
206 deprecated,
207 reason = "with_temp_euid() remains exported for backward compatibility whilst deprecated"
208)]
209pub use privileges::with_temp_euid;
210#[cfg(all(
211 unix,
212 any(
213 target_os = "linux",
214 target_os = "android",
215 target_os = "freebsd",
216 target_os = "openbsd",
217 target_os = "dragonfly",
218 ),
219))]
220pub use privileges::{
221 default_paths_for,
222 default_root_for,
223 make_data_dir_private,
224 make_dir_accessible,
225 nobody_uid,
226};
227use serde::{Deserialize, Serialize};
228
229#[doc(hidden)]
230pub use crate::env::ScopedEnv;
231use crate::error::{ConfigError, ConfigResult};
232/// Resolves a path to an ambient directory handle paired with the relative path component.
233///
234/// This function provides capability-based filesystem access by opening paths relative to
235/// ambient authority. Absolute paths are opened relative to their parent directory; relative
236/// paths reuse the current working directory.
237///
238/// # Returns
239///
240/// Returns a tuple containing:
241/// - A [`cap_std::fs::Dir`] handle for the parent directory
242/// - A [`camino::Utf8PathBuf`] with the relative component
243///
244/// For absolute paths like `/foo/bar`, returns `(Dir("/foo"), "bar")`.
245/// For relative paths like `baz/qux`, returns `(Dir("."), "baz/qux")`.
246/// For root paths like `/`, returns `(Dir("/"), "")` with an empty relative component.
247///
248/// # Errors
249///
250/// Returns an error if the path cannot be opened as a directory or if path operations fail.
251///
252/// # Examples
253///
254/// ```no_run
255/// use camino::Utf8Path;
256/// use pg_embedded_setup_unpriv::ambient_dir_and_path;
257///
258/// # fn main() -> color_eyre::Result<()> {
259/// let (dir, relative) = ambient_dir_and_path(Utf8Path::new("./data"))?;
260/// // Use dir handle for capability-based operations on relative path
261/// # Ok(())
262/// # }
263/// ```
264pub use crate::fs::ambient_dir_and_path;
265
266/// Captures `PostgreSQL` settings supplied via environment variables.
267#[derive(Debug, Clone, Serialize, Deserialize, OrthoConfig, Default)]
268#[ortho_config(prefix = "PG")]
269///
270/// # Examples
271/// ```
272/// use pg_embedded_setup_unpriv::PgEnvCfg;
273///
274/// let cfg = PgEnvCfg::default();
275/// assert!(cfg.port.is_none());
276/// ```
277pub struct PgEnvCfg {
278 /// Optional semver requirement that constrains the `PostgreSQL` version.
279 pub version_req: Option<String>,
280 /// Port assigned to the embedded `PostgreSQL` server.
281 pub port: Option<u16>,
282 /// Name of the administrative user created for the cluster.
283 pub superuser: Option<String>,
284 /// Password provisioned for the administrative user.
285 pub password: Option<String>,
286 /// Directory used for `PostgreSQL` data files when provided.
287 pub data_dir: Option<Utf8PathBuf>,
288 /// Directory containing the `PostgreSQL` binaries when provided.
289 pub runtime_dir: Option<Utf8PathBuf>,
290 /// Locale applied to `initdb` when specified.
291 pub locale: Option<String>,
292 /// Encoding applied to `initdb` when specified.
293 pub encoding: Option<String>,
294 /// Directory for sharing downloaded `PostgreSQL` binaries across test runs.
295 ///
296 /// When `Some`, this explicit path is used directly by `TestCluster`, bypassing
297 /// the automatic resolution chain. When `None`, the cache directory is resolved
298 /// in the following order:
299 ///
300 /// 1. `PG_BINARY_CACHE_DIR` environment variable (if set and non-empty)
301 /// 2. `$XDG_CACHE_HOME/pg-embedded/binaries` (if `XDG_CACHE_HOME` is set)
302 /// 3. `$HOME/.cache/pg-embedded/binaries` (if `HOME` is set)
303 /// 4. `/tmp/pg-embedded/binaries` (final fallback)
304 pub binary_cache_dir: Option<Utf8PathBuf>,
305 /// Base directory under which the default `install` and `data`
306 /// directories are created (`PG_EMBED_ROOT`).
307 ///
308 /// When unset the base is `/var/tmp/pg-embed-{uid}`, which every project
309 /// on a host shares per user. Set it to give a project or a test run its
310 /// own tree. `PG_RUNTIME_DIR` and `PG_DATA_DIR` still override the two
311 /// leaves individually.
312 pub embed_root: Option<Utf8PathBuf>,
313 /// `max_connections` for the server (`PG_MAX_CONNECTIONS`).
314 ///
315 /// Test clusters default to 20; set this when parallel test runners need
316 /// more. Applied to every bootstrap, test or not. Values below
317 /// [`MIN_MAX_CONNECTIONS`] are rejected when the settings are built.
318 pub max_connections: Option<u32>,
319 /// Comma-separated extension names to install before the server starts
320 /// (`PG_EXTENSIONS`, for example `vector`). Empty or unset disables the
321 /// extension hook.
322 pub extensions: Option<String>,
323 /// Location of the extension manifest (`PG_EXTENSIONS_MANIFEST`): an
324 /// `https://` URL, a loopback `http://` URL, or a filesystem path, which
325 /// is the rule the archives it names go through. Required when
326 /// `extensions` is set.
327 pub extensions_manifest: Option<String>,
328 /// Lower-case hex SHA-256 of the manifest bytes
329 /// (`PG_EXTENSIONS_MANIFEST_SHA256`). Required for a URL manifest,
330 /// loopback included; optional for a filesystem path.
331 pub extensions_manifest_sha256: Option<String>,
332 /// Directory holding verified extension archives between runs
333 /// (`PG_EXTENSIONS_CACHE_DIR`). Defaults to
334 /// `$XDG_CACHE_HOME/pg-embedded/extensions` with the same fallbacks as
335 /// [`binary_cache_dir`](Self::binary_cache_dir).
336 pub extensions_cache_dir: Option<Utf8PathBuf>,
337}
338
339impl PgEnvCfg {
340 /// Loads configuration from environment variables without parsing CLI arguments.
341 ///
342 /// # Errors
343 /// Returns an error when environment parsing fails or derived configuration
344 /// cannot be represented using UTF-8 paths.
345 pub fn load() -> ConfigResult<Self> {
346 let args = [OsString::from("pg-embedded-setup-unpriv")];
347 Self::load_from_iter(args).map_err(|err| ConfigError::from(eyre!(err)))
348 }
349
350 /// Converts the configuration into a complete `postgresql_embedded::Settings` object.
351 ///
352 /// Applies version, connection, path, and locale settings from the current configuration.
353 /// Returns an error if the version requirement is invalid. This variant does not apply
354 /// test-specific worker limits.
355 ///
356 /// # Examples
357 /// ```no_run
358 /// use pg_embedded_setup_unpriv::PgEnvCfg;
359 ///
360 /// let cfg = PgEnvCfg::default();
361 /// let settings = cfg.to_settings()?;
362 /// # Ok::<(), pg_embedded_setup_unpriv::Error>(())
363 /// ```
364 ///
365 /// # Returns
366 /// A fully configured `Settings` instance on success, or an error if configuration fails.
367 ///
368 /// # Errors
369 /// Returns an error when the semantic version requirement cannot be parsed,
370 /// or when `PG_MAX_CONNECTIONS` is below [`MIN_MAX_CONNECTIONS`].
371 pub fn to_settings(&self) -> Result<Settings> { self.to_settings_with_context(false) }
372
373 /// Converts the configuration into `Settings`, applying test-only worker limits.
374 ///
375 /// Use this helper for ephemeral test clusters where resource limits are desirable.
376 ///
377 /// # Examples
378 /// ```no_run
379 /// use pg_embedded_setup_unpriv::PgEnvCfg;
380 ///
381 /// let cfg = PgEnvCfg::default();
382 /// let settings = cfg.to_settings_for_tests()?;
383 /// # Ok::<(), pg_embedded_setup_unpriv::Error>(())
384 /// ```
385 ///
386 /// # Errors
387 /// Returns an error when the semantic version requirement cannot be parsed,
388 /// or when `PG_MAX_CONNECTIONS` is below [`MIN_MAX_CONNECTIONS`].
389 pub fn to_settings_for_tests(&self) -> Result<Settings> { self.to_settings_with_context(true) }
390
391 /// Converts the configuration into `Settings`, optionally applying test limits.
392 ///
393 /// Set `for_tests` to `true` to apply the worker limits intended for ephemeral
394 /// test clusters.
395 ///
396 /// # Examples
397 /// ```no_run
398 /// use pg_embedded_setup_unpriv::PgEnvCfg;
399 ///
400 /// let cfg = PgEnvCfg::default();
401 /// let settings = cfg.to_settings_with_context(true)?;
402 /// # Ok::<(), pg_embedded_setup_unpriv::Error>(())
403 /// ```
404 ///
405 /// # Errors
406 /// Returns an error when the semantic version requirement cannot be parsed,
407 /// or when `PG_MAX_CONNECTIONS` is below [`MIN_MAX_CONNECTIONS`].
408 pub fn to_settings_with_context(&self, for_tests: bool) -> Result<Settings> {
409 // Disable the internal postgresql_embedded timeout. This crate wraps lifecycle
410 // operations with tokio::time::timeout using setup_timeout/start_timeout from
411 // TestBootstrapSettings, providing consistent timeout behaviour for both
412 // privileged (subprocess) and unprivileged (in-process) execution paths.
413 // The default 5-second timeout is too short for initdb on slower systems.
414 let mut s = Settings {
415 timeout: None,
416 ..Settings::default()
417 };
418
419 self.apply_version(&mut s)?;
420 self.apply_connection(&mut s);
421 self.apply_paths(&mut s);
422 self.apply_locale(&mut s);
423 if for_tests {
424 Self::apply_worker_limits(&mut s);
425 }
426 self.apply_max_connections(&mut s)?;
427
428 Ok(s)
429 }
430
431 fn apply_version(&self, settings: &mut Settings) -> ConfigResult<()> {
432 if let Some(ref vr) = self.version_req {
433 settings.version =
434 VersionReq::parse(vr).context("PG_VERSION_REQ invalid semver spec")?;
435 }
436 Ok(())
437 }
438
439 fn apply_connection(&self, settings: &mut Settings) {
440 if let Some(p) = self.port {
441 settings.port = p;
442 }
443 if let Some(ref u) = self.superuser {
444 settings.username.clone_from(u);
445 }
446 if let Some(ref pw) = self.password {
447 settings.password.clone_from(pw);
448 }
449 }
450
451 fn apply_paths(&self, settings: &mut Settings) {
452 if let Some(ref dir) = self.data_dir {
453 settings.data_dir = dir.clone().into_std_path_buf();
454 }
455 if let Some(ref dir) = self.runtime_dir {
456 settings.installation_dir = dir.clone().into_std_path_buf();
457 }
458 }
459
460 /// Applies locale and encoding settings to the `PostgreSQL` configuration if specified
461 /// in the environment.
462 ///
463 /// Inserts the `locale` and `encoding` values into the settings configuration map when
464 /// present in the environment configuration.
465 fn apply_locale(&self, settings: &mut Settings) {
466 if let Some(ref loc) = self.locale {
467 settings.configuration.insert("locale".into(), loc.clone());
468 }
469 if let Some(ref enc) = self.encoding {
470 settings
471 .configuration
472 .insert("encoding".into(), enc.clone());
473 }
474 }
475
476 /// Applies an explicit `max_connections`, replacing the test default of 20.
477 ///
478 /// `PostgreSQL` keeps `superuser_reserved_connections` (3 by default) below
479 /// `max_connections`, so anything under [`MIN_MAX_CONNECTIONS`] would make
480 /// the server refuse to start; reject it here with a clear message.
481 fn apply_max_connections(&self, settings: &mut Settings) -> ConfigResult<()> {
482 let Some(limit) = self.max_connections else {
483 return Ok(());
484 };
485 if limit < MIN_MAX_CONNECTIONS {
486 return Err(ConfigError::from(eyre!(
487 "PG_MAX_CONNECTIONS must be at least {MIN_MAX_CONNECTIONS} (got {limit}); \
488 PostgreSQL reserves three superuser slots below max_connections"
489 )));
490 }
491 settings
492 .configuration
493 .insert("max_connections".into(), limit.to_string());
494 Ok(())
495 }
496
497 fn apply_worker_limits(settings: &mut Settings) {
498 for (key, value) in WORKER_LIMIT_DEFAULTS {
499 settings
500 .configuration
501 .entry(key.to_owned())
502 .or_insert_with(|| value.to_owned());
503 }
504 }
505}
506
507/// Smallest `PG_MAX_CONNECTIONS` the helper accepts: one ordinary slot above
508/// `PostgreSQL`'s default of three reserved superuser connections.
509pub const MIN_MAX_CONNECTIONS: u32 = 4;
510const WORKER_LIMIT_DEFAULTS: [(&str, &str); 8] = [
511 ("max_connections", "20"),
512 ("max_worker_processes", "2"),
513 ("max_parallel_workers", "0"),
514 ("max_parallel_workers_per_gather", "0"),
515 ("max_parallel_maintenance_workers", "0"),
516 ("autovacuum", "off"),
517 ("max_wal_senders", "0"),
518 ("max_replication_slots", "0"),
519];