// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! `fabrica migrate` and `fabrica doctor` — schema application and a startup //! health check. use std::path::Path; use std::process::{Command, ExitCode}; use config::Config; use crate::context::block_on; /// The minimum git version fabrica supports (pack transport relies on it). const MIN_GIT: (u32, u32) = (2, 41); /// Run `fabrica migrate`: connect and apply all pending migrations. pub(crate) fn migrate(config_path: Option<&Path>) -> anyhow::Result { block_on(async { let (_config, _store) = crate::context::open_store(config_path).await?; // `open_store` already migrated on connect; reaching here means success. println!("migrations applied"); anyhow::Ok(ExitCode::SUCCESS) }) } /// Run `fabrica doctor`: verify the environment fabrica needs to serve. /// /// Checks the `git` binary and version, that the configuration loads, and that the /// database is reachable and migratable. `--quiet` suppresses the per-check report /// and only sets the exit code, for use as a container health check. pub(crate) fn doctor(config_path: Option<&Path>, quiet: bool) -> anyhow::Result { let mut ok = true; let mut check = |label: &str, result: Result| { match &result { Ok(detail) => { if !quiet { println!("ok {label}: {detail}"); } } Err(detail) => { ok = false; // Failures are always reported, even in quiet mode, on stderr. eprintln!("FAIL {label}: {detail}"); } } }; // Configuration must load before anything else can be checked. let loaded = match config::load(config_path) { Ok(loaded) => loaded, Err(err) => { eprintln!("FAIL config: {err}"); return Ok(ExitCode::FAILURE); } }; check("config", Ok("loaded".to_string())); check("git", check_git(&loaded.config)); check("storage", Ok(describe_storage(&loaded.config))); let database = block_on(async { anyhow::Ok(check_database(&loaded.config).await) })?; check("database", database); if ok { if !quiet { println!("all checks passed"); } Ok(ExitCode::SUCCESS) } else { Ok(ExitCode::FAILURE) } } /// Describe the upload storage backend (config is already validated by load). fn describe_storage(config: &Config) -> String { match config.storage.backend { config::StorageBackend::Local => { format!("local ({})", config.storage.data_dir.display()) } config::StorageBackend::S3 => match &config.storage.s3 { Some(s3) => { let endpoint = s3.endpoint.as_deref().unwrap_or("aws"); format!( "s3 (bucket {}, region {}, {endpoint})", s3.bucket, s3.region ) } None => "s3 (unconfigured)".to_string(), }, } } /// Verify the configured `git` binary is present and recent enough. fn check_git(config: &Config) -> Result { let output = Command::new(&config.git.binary) .arg("--version") .output() .map_err(|err| format!("cannot run {:?}: {err}", config.git.binary))?; if !output.status.success() { return Err(format!("{:?} --version failed", config.git.binary)); } let text = String::from_utf8_lossy(&output.stdout); let version = parse_git_version(&text) .ok_or_else(|| format!("could not parse git version from {text:?}"))?; if version < MIN_GIT { return Err(format!( "git {}.{} is too old; need >= {}.{}", version.0, version.1, MIN_GIT.0, MIN_GIT.1 )); } Ok(format!("{}.{} ({})", version.0, version.1, text.trim())) } /// Extract `(major, minor)` from a `git version 2.43.0` string. fn parse_git_version(text: &str) -> Option<(u32, u32)> { let version = text.split_whitespace().nth(2)?; let mut parts = version.split('.'); let major = parts.next()?.parse().ok()?; let minor = parts.next()?.parse().ok()?; Some((major, minor)) } /// Verify the database opens and migrations apply. async fn check_database(config: &Config) -> Result { let store = store::Store::connect(&config.database.url, config.database.max_connections) .await .map_err(|err| format!("connect failed: {err}"))?; store .migrate() .await .map_err(|err| format!("migrate failed: {err}"))?; Ok(format!("{:?} reachable", store.backend())) } #[cfg(test)] mod tests { #![allow(clippy::unwrap_used)] use super::*; #[test] fn parses_git_version() { assert_eq!(parse_git_version("git version 2.43.0"), Some((2, 43))); assert_eq!( parse_git_version("git version 2.41.0.windows.1"), Some((2, 41)) ); assert_eq!(parse_git_version("nonsense"), None); } }