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)?;