pg_embedded_setup_unpriv/bootstrap/
mod.rs1mod env;
6mod env_types;
7mod mode;
8mod prepare;
9
10use std::time::Duration;
11
12use color_eyre::eyre::{Context, eyre};
13use postgresql_embedded::Settings;
14use serde::{Deserialize, Serialize};
15
16use crate::{
17 PgEnvCfg,
18 error::{BootstrapResult, Result as CrateResult},
19};
20
21pub use env::{TestBootstrapEnvironment, find_timezone_dir};
22pub use mode::{ExecutionMode, ExecutionPrivileges, detect_execution_privileges};
23
24use self::{
25 env::{shutdown_timeout_from_env, worker_binary_from_env},
26 mode::determine_execution_mode,
27 prepare::prepare_bootstrap,
28};
29
30const DEFAULT_SETUP_TIMEOUT: Duration = Duration::from_secs(180);
31const DEFAULT_START_TIMEOUT: Duration = Duration::from_secs(60);
32
33#[derive(Clone, Copy)]
34enum BootstrapKind {
35 Default,
36 Test,
37}
38
39#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Default)]
41pub enum CleanupMode {
42 #[default]
44 DataOnly,
45 Full,
47 None,
49}
50
51#[derive(Debug, Clone)]
53pub struct TestBootstrapSettings {
54 pub privileges: ExecutionPrivileges,
56 pub execution_mode: ExecutionMode,
58 pub settings: Settings,
60 pub environment: TestBootstrapEnvironment,
62 pub worker_binary: Option<camino::Utf8PathBuf>,
64 pub setup_timeout: Duration,
66 pub start_timeout: Duration,
68 pub shutdown_timeout: Duration,
70 pub cleanup_mode: CleanupMode,
72 pub binary_cache_dir: Option<camino::Utf8PathBuf>,
77}
78
79pub fn run() -> CrateResult<()> {
109 orchestrate_bootstrap(BootstrapKind::Default)?;
110 Ok(())
111}
112
113pub fn bootstrap_for_tests() -> BootstrapResult<TestBootstrapSettings> {
134 orchestrate_bootstrap(BootstrapKind::Test)
135}
136
137fn orchestrate_bootstrap(kind: BootstrapKind) -> BootstrapResult<TestBootstrapSettings> {
138 install_color_eyre();
139 if matches!(kind, BootstrapKind::Test) {
140 validate_backend_selection()?;
141 }
142
143 let privileges = detect_execution_privileges();
144 let cfg = PgEnvCfg::load().context("failed to load configuration via OrthoConfig")?;
145 let settings = match kind {
146 BootstrapKind::Default => cfg.to_settings()?,
147 BootstrapKind::Test => cfg.to_settings_for_tests()?,
148 };
149 let worker_binary = worker_binary_from_env(privileges)?;
150 let execution_mode = determine_execution_mode(privileges, worker_binary.as_ref())?;
151 let shutdown_timeout = shutdown_timeout_from_env()?;
152 let prepared = prepare_bootstrap(privileges, settings, &cfg)?;
153
154 Ok(TestBootstrapSettings {
155 privileges,
156 execution_mode,
157 settings: prepared.settings,
158 environment: prepared.environment,
159 worker_binary,
160 setup_timeout: DEFAULT_SETUP_TIMEOUT,
161 start_timeout: DEFAULT_START_TIMEOUT,
162 shutdown_timeout,
163 cleanup_mode: CleanupMode::default(),
164 binary_cache_dir: cfg.binary_cache_dir,
165 })
166}
167
168fn install_color_eyre() {
169 if let Err(err) = color_eyre::install() {
170 tracing::debug!("color_eyre already installed: {err}");
171 }
172}
173
174fn validate_backend_selection() -> BootstrapResult<()> {
175 let raw = std::env::var_os("PG_TEST_BACKEND").unwrap_or_default();
176 let value = raw.to_string_lossy();
177 let trimmed = value.trim();
178 if trimmed.is_empty() || trimmed == "postgresql_embedded" {
179 return Ok(());
180 }
181 Err(eyre!(
182 "SKIP-TEST-CLUSTER: unsupported PG_TEST_BACKEND '{trimmed}'; supported backends: postgresql_embedded"
183 )
184 .into())
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190 use crate::test_support::scoped_env;
191 use camino::Utf8PathBuf;
192 use rstest::{fixture, rstest};
193 use std::ffi::OsString;
194 use tempfile::tempdir;
195
196 fn env_vars<const N: usize>(
198 pairs: [(&str, Option<&str>); N],
199 ) -> Vec<(OsString, Option<OsString>)> {
200 pairs
201 .into_iter()
202 .map(|(k, v)| (OsString::from(k), v.map(OsString::from)))
203 .collect()
204 }
205
206 #[test]
207 fn orchestrate_bootstrap_respects_env_overrides() {
208 if detect_execution_privileges() == ExecutionPrivileges::Root {
209 tracing::warn!(
210 "skipping orchestrate test because root privileges require PG_EMBEDDED_WORKER"
211 );
212 return;
213 }
214
215 let runtime = tempdir().expect("runtime dir");
216 let data = tempdir().expect("data dir");
217 let runtime_path =
218 Utf8PathBuf::from_path_buf(runtime.path().to_path_buf()).expect("runtime dir utf8");
219 let data_path =
220 Utf8PathBuf::from_path_buf(data.path().to_path_buf()).expect("data dir utf8");
221
222 let _guard = scoped_env(env_vars([
223 ("PG_RUNTIME_DIR", Some(runtime_path.as_str())),
224 ("PG_DATA_DIR", Some(data_path.as_str())),
225 ("PG_SUPERUSER", Some("bootstrap_test")),
226 ("PG_PASSWORD", Some("bootstrap_test_pw")),
227 ("PG_EMBEDDED_WORKER", None),
228 ]));
229 let settings = orchestrate_bootstrap(BootstrapKind::Default).expect("bootstrap to succeed");
230
231 assert_paths(&settings, &runtime_path, &data_path);
232 assert_identity(&settings, "bootstrap_test", "bootstrap_test_pw");
233 assert_environment(&settings, &runtime_path);
234 }
235
236 struct RunTestPaths {
238 _runtime: tempfile::TempDir,
239 _data: tempfile::TempDir,
240 runtime_path: Utf8PathBuf,
241 data_path: Utf8PathBuf,
242 }
243
244 #[fixture]
246 fn run_test_paths() -> Option<RunTestPaths> {
247 if detect_execution_privileges() == ExecutionPrivileges::Root {
248 tracing::warn!("skipping run test because root privileges require PG_EMBEDDED_WORKER");
249 return None;
250 }
251
252 let runtime = tempdir().expect("runtime dir");
253 let data = tempdir().expect("data dir");
254 let runtime_path =
255 Utf8PathBuf::from_path_buf(runtime.path().to_path_buf()).expect("runtime dir utf8");
256 let data_path =
257 Utf8PathBuf::from_path_buf(data.path().to_path_buf()).expect("data dir utf8");
258
259 Some(RunTestPaths {
260 _runtime: runtime,
261 _data: data,
262 runtime_path,
263 data_path,
264 })
265 }
266
267 #[rstest]
268 fn run_succeeds_with_customised_paths(run_test_paths: Option<RunTestPaths>) {
269 let Some(paths) = run_test_paths else {
270 return;
271 };
272
273 let _guard = scoped_env(env_vars([
274 ("PG_RUNTIME_DIR", Some(paths.runtime_path.as_str())),
275 ("PG_DATA_DIR", Some(paths.data_path.as_str())),
276 ("PG_SUPERUSER", Some("bootstrap_run")),
277 ("PG_PASSWORD", Some("bootstrap_run_pw")),
278 ("PG_EMBEDDED_WORKER", None),
279 ]));
280
281 run().expect("run should bootstrap successfully");
282
283 assert!(
284 paths.runtime_path.join("cache").exists(),
285 "cache directory should be created"
286 );
287 assert!(
288 paths.runtime_path.join("run").exists(),
289 "runtime directory should be created"
290 );
291 }
292
293 struct BootstrapPaths {
295 _runtime: tempfile::TempDir,
296 _data: tempfile::TempDir,
297 _cache: tempfile::TempDir,
298 runtime_path: Utf8PathBuf,
299 data_path: Utf8PathBuf,
300 cache_path: Utf8PathBuf,
301 }
302
303 #[fixture]
305 fn bootstrap_paths() -> Option<BootstrapPaths> {
306 if detect_execution_privileges() == ExecutionPrivileges::Root {
307 tracing::warn!(
308 "skipping orchestrate test because root privileges require PG_EMBEDDED_WORKER"
309 );
310 return None;
311 }
312
313 let runtime = tempdir().expect("runtime dir");
314 let data = tempdir().expect("data dir");
315 let cache = tempdir().expect("cache dir");
316 let runtime_path =
317 Utf8PathBuf::from_path_buf(runtime.path().to_path_buf()).expect("runtime dir utf8");
318 let data_path =
319 Utf8PathBuf::from_path_buf(data.path().to_path_buf()).expect("data dir utf8");
320 let cache_path =
321 Utf8PathBuf::from_path_buf(cache.path().to_path_buf()).expect("cache dir utf8");
322
323 Some(BootstrapPaths {
324 _runtime: runtime,
325 _data: data,
326 _cache: cache,
327 runtime_path,
328 data_path,
329 cache_path,
330 })
331 }
332
333 fn orchestrate_with_cache_env(paths: &BootstrapPaths) -> TestBootstrapSettings {
337 let _guard = scoped_env(env_vars([
338 ("PG_RUNTIME_DIR", Some(paths.runtime_path.as_str())),
339 ("PG_DATA_DIR", Some(paths.data_path.as_str())),
340 ("PG_BINARY_CACHE_DIR", Some(paths.cache_path.as_str())),
341 ("PG_SUPERUSER", Some("cache_test")),
342 ("PG_PASSWORD", Some("cache_test_pw")),
343 ("PG_EMBEDDED_WORKER", None),
344 ]));
345 orchestrate_bootstrap(BootstrapKind::Default).expect("bootstrap to succeed")
346 }
347
348 #[rstest]
349 fn orchestrate_bootstrap_propagates_binary_cache_dir(bootstrap_paths: Option<BootstrapPaths>) {
350 let Some(paths) = bootstrap_paths else {
351 return;
352 };
353
354 let settings = orchestrate_with_cache_env(&paths);
355
356 assert_eq!(
357 settings.binary_cache_dir,
358 Some(paths.cache_path.clone()),
359 "binary_cache_dir should propagate from PG_BINARY_CACHE_DIR"
360 );
361 }
362
363 fn assert_paths(
364 settings: &TestBootstrapSettings,
365 runtime_path: &Utf8PathBuf,
366 data_path: &Utf8PathBuf,
367 ) {
368 let observed_install =
369 Utf8PathBuf::from_path_buf(settings.settings.installation_dir.clone())
370 .expect("installation dir utf8");
371 let observed_data =
372 Utf8PathBuf::from_path_buf(settings.settings.data_dir.clone()).expect("data dir utf8");
373
374 assert_eq!(observed_install.as_path(), runtime_path.as_path());
375 assert_eq!(observed_data.as_path(), data_path.as_path());
376 }
377
378 fn assert_identity(
379 settings: &TestBootstrapSettings,
380 expected_user: &str,
381 expected_password: &str,
382 ) {
383 assert_eq!(settings.settings.username, expected_user);
384 assert_eq!(settings.settings.password, expected_password);
385 assert_eq!(settings.privileges, ExecutionPrivileges::Unprivileged);
386 assert_eq!(settings.execution_mode, ExecutionMode::InProcess);
387 assert!(settings.worker_binary.is_none());
388 }
389
390 fn assert_environment(settings: &TestBootstrapSettings, runtime_path: &Utf8PathBuf) {
391 let env_pairs = settings.environment.to_env();
392 let pgpass = runtime_path.join(".pgpass");
393 assert!(env_pairs.contains(&("PGPASSFILE".into(), Some(pgpass.as_str().into()))));
394 assert_eq!(settings.environment.home.as_path(), runtime_path.as_path());
395 }
396
397 #[rstest]
398 #[case::unset(None, true)]
399 #[case::empty(Some(""), true)]
400 #[case::embedded(Some("postgresql_embedded"), true)]
401 #[case::unsupported(Some("sqlite"), false)]
402 fn validate_backend_selection_respects_pg_test_backend(
403 #[case] backend: Option<&str>,
404 #[case] should_succeed: bool,
405 ) {
406 let _guard = scoped_env(env_vars([("PG_TEST_BACKEND", backend)]));
407 let result = validate_backend_selection();
408 assert_eq!(
409 result.is_ok(),
410 should_succeed,
411 "unexpected backend validation result for {backend:?}"
412 );
413 if !should_succeed {
414 let err = result.expect_err("expected backend validation to fail");
415 assert!(
416 err.to_string().contains("SKIP-TEST-CLUSTER"),
417 "expected SKIP-TEST-CLUSTER in error message, got {err:?}"
418 );
419 }
420 }
421}