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;
19pub use prepare::{
20 PasswordReuseOutcome,
21 default_paths_under,
22 reuse_existing_password,
23 stored_cluster_password,
24};
25use serde::{Deserialize, Serialize};
26
27use self::{
28 env::{shutdown_timeout_from_env, worker_binary_from_env},
29 mode::determine_execution_mode,
30 prepare::prepare_bootstrap,
31};
32use crate::{
33 PgEnvCfg,
34 error::{BootstrapResult, Result as CrateResult},
35 extensions::{ExtensionRequest, InstalledExtension},
36};
37
38const DEFAULT_SETUP_TIMEOUT: Duration = Duration::from_secs(180);
39const DEFAULT_START_TIMEOUT: Duration = Duration::from_secs(60);
40
41#[cfg(test)]
42type SetupOnlyLifecycleHook =
43 Arc<dyn Fn(TestBootstrapSettings) -> BootstrapResult<()> + Send + Sync>;
44
45#[cfg(test)]
46static SETUP_ONLY_LIFECYCLE_HOOK: OnceLock<Mutex<Option<SetupOnlyLifecycleHook>>> = OnceLock::new();
47
48#[derive(Clone, Copy)]
49enum BootstrapKind {
50 Default,
51 Test,
52}
53
54#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Default)]
56pub enum CleanupMode {
57 #[default]
59 DataOnly,
60 Full,
62 None,
64}
65
66#[derive(Debug, Clone)]
68pub struct TestBootstrapSettings {
69 pub privileges: ExecutionPrivileges,
71 pub execution_mode: ExecutionMode,
73 pub settings: Settings,
75 pub environment: TestBootstrapEnvironment,
77 pub worker_binary: Option<camino::Utf8PathBuf>,
79 pub setup_timeout: Duration,
81 pub start_timeout: Duration,
83 pub shutdown_timeout: Duration,
85 pub cleanup_mode: CleanupMode,
87 pub binary_cache_dir: Option<camino::Utf8PathBuf>,
92 pub extensions: Option<ExtensionRequest>,
94 pub installed_extensions: Vec<InstalledExtension>,
96}
97
98pub fn run() -> CrateResult<()> {
131 let bootstrap = orchestrate_bootstrap(BootstrapKind::Default)?;
132 run_setup_only_lifecycle(bootstrap)
133}
134
135pub fn bootstrap_for_tests() -> BootstrapResult<TestBootstrapSettings> {
159 orchestrate_bootstrap(BootstrapKind::Test)
160}
161
162fn orchestrate_bootstrap(kind: BootstrapKind) -> BootstrapResult<TestBootstrapSettings> {
163 install_color_eyre();
164 if matches!(kind, BootstrapKind::Test) {
165 validate_backend_selection()?;
166 }
167
168 let privileges = detect_execution_privileges();
169 let cfg = PgEnvCfg::load().context("failed to load configuration via OrthoConfig")?;
170 let settings = match kind {
171 BootstrapKind::Default => cfg.to_settings()?,
172 BootstrapKind::Test => cfg.to_settings_for_tests()?,
173 };
174 let worker_binary = worker_binary_from_env(privileges)?;
175 let execution_mode = determine_execution_mode(privileges, worker_binary.as_ref())?;
176 let shutdown_timeout = shutdown_timeout_from_env()?;
177 let extensions = ExtensionRequest::from_config(&cfg)?;
178 let prepared = prepare_bootstrap(privileges, settings, &cfg)?;
179
180 Ok(TestBootstrapSettings {
181 privileges,
182 execution_mode,
183 settings: prepared.settings,
184 environment: prepared.environment,
185 worker_binary,
186 setup_timeout: DEFAULT_SETUP_TIMEOUT,
187 start_timeout: DEFAULT_START_TIMEOUT,
188 shutdown_timeout,
189 cleanup_mode: CleanupMode::default(),
190 binary_cache_dir: cfg.binary_cache_dir,
191 extensions,
192 installed_extensions: Vec::new(),
193 })
194}
195
196fn install_color_eyre() {
197 if let Err(err) = color_eyre::install() {
198 tracing::debug!("color_eyre already installed: {err}");
199 }
200}
201
202fn validate_backend_selection() -> BootstrapResult<()> {
203 let raw = std::env::var_os("PG_TEST_BACKEND").unwrap_or_default();
204 let value = raw.to_string_lossy();
205 let trimmed = value.trim();
206 if trimmed.is_empty() || trimmed == "postgresql_embedded" {
207 return Ok(());
208 }
209 Err(eyre!(
210 "SKIP-TEST-CLUSTER: unsupported PG_TEST_BACKEND '{trimmed}'; supported backends: \
211 postgresql_embedded"
212 )
213 .into())
214}
215
216fn run_setup_only_lifecycle(bootstrap: TestBootstrapSettings) -> CrateResult<()> {
221 #[cfg(test)]
222 {
223 let installed_setup_hook = SETUP_ONLY_LIFECYCLE_HOOK
224 .get_or_init(|| Mutex::new(None))
225 .lock()
226 .unwrap_or_else(std::sync::PoisonError::into_inner)
227 .clone();
228 if let Some(setup_hook) = installed_setup_hook {
229 return setup_hook(bootstrap).map_err(Into::into);
230 }
231 }
232
233 crate::cluster::setup_postgres_only(bootstrap)?;
234 Ok(())
235}
236
237#[cfg(test)]
238pub(crate) struct SetupOnlyLifecycleHookGuard;
239
240#[cfg(test)]
241impl Drop for SetupOnlyLifecycleHookGuard {
242 fn drop(&mut self) {
243 let mut slot = SETUP_ONLY_LIFECYCLE_HOOK
244 .get_or_init(|| Mutex::new(None))
245 .lock()
246 .unwrap_or_else(std::sync::PoisonError::into_inner);
247 slot.take();
248 }
249}
250
251#[cfg(test)]
252pub(crate) fn install_setup_only_lifecycle_hook<F>(hook: F) -> SetupOnlyLifecycleHookGuard
253where
254 F: Fn(TestBootstrapSettings) -> BootstrapResult<()> + Send + Sync + 'static,
255{
256 let mut slot = SETUP_ONLY_LIFECYCLE_HOOK
257 .get_or_init(|| Mutex::new(None))
258 .lock()
259 .unwrap_or_else(std::sync::PoisonError::into_inner);
260 *slot = Some(Arc::new(hook));
261 SetupOnlyLifecycleHookGuard
262}
263
264#[cfg(test)]
265mod mod_tests;