diff --git a/crates/lib/src/install/config.rs b/crates/lib/src/install/config.rs index 7f123e45f2..3ed0eca337 100644 --- a/crates/lib/src/install/config.rs +++ b/crates/lib/src/install/config.rs @@ -287,6 +287,20 @@ impl InstallConfiguration { } } +/// Read a configuration fragment found by a directory scan, returning `None` +/// if it was removed since the scan: `/run` and `/etc` are writable, so a +/// fragment can vanish while we're loading, and that shouldn't be fatal. +fn read_fragment(path: &std::path::Path) -> Result> { + match std::fs::read_to_string(path) { + Ok(buf) => Ok(Some(buf)), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + tracing::debug!("Skipping vanished config fragment {path:?}"); + Ok(None) + } + Err(e) => Err(e).with_context(|| format!("Reading {path:?}")), + } +} + #[context("Loading configuration")] /// Load the install configuration, merging all found configuration files. pub(crate) fn load_config() -> Result> { @@ -297,7 +311,9 @@ pub(crate) fn load_config() -> Result> { let fragments = liboverdrop::scan(SYSTEMD_CONVENTIONAL_BASES, "bootc/install", &["toml"], true); let mut config: Option = None; for (_name, path) in fragments { - let buf = std::fs::read_to_string(&path)?; + let Some(buf) = read_fragment(&path)? else { + continue; + }; let mut unused = std::collections::HashSet::new(); let de = toml::Deserializer::parse(&buf).with_context(|| format!("Parsing {path:?}"))?; let mut c: InstallConfigurationToplevel = serde_ignored::deserialize(de, |path| { @@ -337,6 +353,19 @@ pub(crate) fn load_config() -> Result> { mod tests { use super::*; + #[test] + fn test_read_fragment() -> Result<()> { + let td = tempfile::tempdir()?; + let present = td.path().join("10-present.toml"); + std::fs::write(&present, "[install]\n")?; + assert_eq!(read_fragment(&present)?.as_deref(), Some("[install]\n")); + assert_eq!(read_fragment(&td.path().join("20-vanished.toml"))?, None); + // Errors other than ENOENT are still fatal, and name the path + let err = read_fragment(td.path()).unwrap_err(); + assert!(format!("{err:#}").contains("Reading"), "{err:#}"); + Ok(()) + } + #[test] /// Verify that we can parse our default config file fn test_parse_config() { diff --git a/crates/tests-integration/src/container.rs b/crates/tests-integration/src/container.rs index 08716f475a..9e2392db44 100644 --- a/crates/tests-integration/src/container.rs +++ b/crates/tests-integration/src/container.rs @@ -4,6 +4,7 @@ use indoc::indoc; use scopeguard::defer; use serde::Deserialize; use std::process::{Command, Stdio}; +use std::sync::Mutex; use std::{fs, path::Path}; use anyhow::{Context, Result}; @@ -114,7 +115,15 @@ pub(crate) fn test_bootc_upgrade() -> Result<()> { Ok(()) } +/// Serializes the tests that read the install config against the one that +/// adds a fragment to /run/bootc/install, since libtest runs them concurrently +/// and the other readers would otherwise see its temporary config. +static INSTALL_CONFIG_LOCK: Mutex<()> = Mutex::new(()); + pub(crate) fn test_bootc_install_config() -> Result<()> { + let _guard = INSTALL_CONFIG_LOCK + .lock() + .unwrap_or_else(|e| e.into_inner()); let sh = &xshell::Shell::new()?; let config = cmd!(sh, "bootc install print-configuration").read()?; let config: serde_json::Value = @@ -137,6 +146,9 @@ pub(crate) fn test_bootc_install_config_all() -> Result<()> { ostree: Option, } + let _guard = INSTALL_CONFIG_LOCK + .lock() + .unwrap_or_else(|e| e.into_inner()); let config_d = std::path::Path::new("/run/bootc/install/"); let test_toml_path = config_d.join("10-test.toml"); std::fs::create_dir_all(&config_d)?;