pg_embedded_setup_unpriv/bootstrap/
mod.rs1mod env;
6mod env_types;
7mod mode;
8mod prepare;
9
10#[cfg(test)]
11use std::sync::{Arc, Mutex, OnceLock};
12use std::time::Duration;
13
14use color_eyre::eyre::{Context, eyre};
15pub use env::{TestBootstrapEnvironment, find_timezone_dir};
16pub use mode::{ExecutionMode, ExecutionPrivileges, detect_execution_privileges};
17pub(crate) use mode::{root_privilege_drop_supported, unsupported_root_privilege_drop_error};
18use postgresql_embedded::Settings;
19use serde::{Deserialize, Serialize};
20
21use self::{
22 env::{shutdown_timeout_from_env, worker_binary_from_env},
23 mode::determine_execution_mode,
24 prepare::prepare_bootstrap,
25};
26use crate::{
27 PgEnvCfg,
28 error::{BootstrapResult, Result as CrateResult},
29};
30
31const DEFAULT_SETUP_TIMEOUT: Duration = Duration::from_secs(180);
32const DEFAULT_START_TIMEOUT: Duration = Duration::from_secs(60);
33
34#[cfg(test)]
35type SetupOnlyLifecycleHook =
36 Arc<dyn Fn(TestBootstrapSettings) -> BootstrapResult<()> + Send + Sync>;
37
38#[cfg(test)]
39static SETUP_ONLY_LIFECYCLE_HOOK: OnceLock<Mutex<Option<SetupOnlyLifecycleHook>>> = OnceLock::new();
40
41#[derive(Clone, Copy)]
42enum BootstrapKind {
43 Default,
44 Test,
45}
46
47#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Default)]
49pub enum CleanupMode {
50 #[default]
52 DataOnly,
53 Full,
55 None,
57}
58
59#[derive(Debug, Clone)]
61pub struct TestBootstrapSettings {
62 pub privileges: ExecutionPrivileges,
64 pub execution_mode: ExecutionMode,
66 pub settings: Settings,
68 pub environment: TestBootstrapEnvironment,
70 pub worker_binary: Option<camino::Utf8PathBuf>,
72 pub setup_timeout: Duration,
74 pub start_timeout: Duration,
76 pub shutdown_timeout: Duration,
78 pub cleanup_mode: CleanupMode,
80 pub binary_cache_dir: Option<camino::Utf8PathBuf>,
85}
86
87pub fn run() -> CrateResult<()> {
118 let bootstrap = orchestrate_bootstrap(BootstrapKind::Default)?;
119 run_setup_only_lifecycle(bootstrap)
120}
121
122pub fn bootstrap_for_tests() -> BootstrapResult<TestBootstrapSettings> {
146 orchestrate_bootstrap(BootstrapKind::Test)
147}
148
149fn orchestrate_bootstrap(kind: BootstrapKind) -> BootstrapResult<TestBootstrapSettings> {
150 install_color_eyre();
151 if matches!(kind, BootstrapKind::Test) {
152 validate_backend_selection()?;
153 }
154
155 let privileges = detect_execution_privileges();
156 let cfg = PgEnvCfg::load().context("failed to load configuration via OrthoConfig")?;
157 let settings = match kind {
158 BootstrapKind::Default => cfg.to_settings()?,
159 BootstrapKind::Test => cfg.to_settings_for_tests()?,
160 };
161 let worker_binary = worker_binary_from_env(privileges)?;
162 let execution_mode = determine_execution_mode(privileges, worker_binary.as_ref())?;
163 let shutdown_timeout = shutdown_timeout_from_env()?;
164 let prepared = prepare_bootstrap(privileges, settings, &cfg)?;
165
166 Ok(TestBootstrapSettings {
167 privileges,
168 execution_mode,
169 settings: prepared.settings,
170 environment: prepared.environment,
171 worker_binary,
172 setup_timeout: DEFAULT_SETUP_TIMEOUT,
173 start_timeout: DEFAULT_START_TIMEOUT,
174 shutdown_timeout,
175 cleanup_mode: CleanupMode::default(),
176 binary_cache_dir: cfg.binary_cache_dir,
177 })
178}
179
180fn install_color_eyre() {
181 if let Err(err) = color_eyre::install() {
182 tracing::debug!("color_eyre already installed: {err}");
183 }
184}
185
186fn validate_backend_selection() -> BootstrapResult<()> {
187 let raw = std::env::var_os("PG_TEST_BACKEND").unwrap_or_default();
188 let value = raw.to_string_lossy();
189 let trimmed = value.trim();
190 if trimmed.is_empty() || trimmed == "postgresql_embedded" {
191 return Ok(());
192 }
193 Err(eyre!(
194 "SKIP-TEST-CLUSTER: unsupported PG_TEST_BACKEND '{trimmed}'; supported backends: \
195 postgresql_embedded"
196 )
197 .into())
198}
199
200fn run_setup_only_lifecycle(bootstrap: TestBootstrapSettings) -> CrateResult<()> {
205 #[cfg(test)]
206 {
207 let installed_setup_hook = SETUP_ONLY_LIFECYCLE_HOOK
208 .get_or_init(|| Mutex::new(None))
209 .lock()
210 .unwrap_or_else(std::sync::PoisonError::into_inner)
211 .clone();
212 if let Some(setup_hook) = installed_setup_hook {
213 return setup_hook(bootstrap).map_err(Into::into);
214 }
215 }
216
217 crate::cluster::setup_postgres_only(bootstrap)?;
218 Ok(())
219}
220
221#[cfg(test)]
222pub(crate) struct SetupOnlyLifecycleHookGuard;
223
224#[cfg(test)]
225impl Drop for SetupOnlyLifecycleHookGuard {
226 fn drop(&mut self) {
227 let mut slot = SETUP_ONLY_LIFECYCLE_HOOK
228 .get_or_init(|| Mutex::new(None))
229 .lock()
230 .unwrap_or_else(std::sync::PoisonError::into_inner);
231 slot.take();
232 }
233}
234
235#[cfg(test)]
236pub(crate) fn install_setup_only_lifecycle_hook<F>(hook: F) -> SetupOnlyLifecycleHookGuard
237where
238 F: Fn(TestBootstrapSettings) -> BootstrapResult<()> + Send + Sync + 'static,
239{
240 let mut slot = SETUP_ONLY_LIFECYCLE_HOOK
241 .get_or_init(|| Mutex::new(None))
242 .lock()
243 .unwrap_or_else(std::sync::PoisonError::into_inner);
244 *slot = Some(Arc::new(hook));
245 SetupOnlyLifecycleHookGuard
246}
247
248#[cfg(test)]
249mod mod_tests;