From f4e71575812d5cddc718c79f34e55d0aa5358960 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Fri, 25 Sep 2026 12:44:57 -0400 Subject: [PATCH 1/2] install/config: Skip config fragments that vanish while loading The install config fragments live partly in writable directories (/etc, /run), so one can be removed between the directory scan and reading it. That made `bootc install print-configuration` (and install itself) fail with a bare "No such file or directory", with no path to say which file. A removed fragment is just not part of the config anymore, so skip it; other read errors now name the file. This is what made the "install config" container integration test flake while "printconfig --all" created and removed a fragment in /run/bootc/install concurrently. Generated-by: AI Signed-off-by: Colin Walters --- crates/lib/src/install/config.rs | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) 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() { From b5e7c08ff7080c21269ca0e5e227768f9ce0eaac Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Fri, 25 Sep 2026 12:45:09 -0400 Subject: [PATCH 2/2] tests-integration: Serialize the install config container tests libtest runs the container tests concurrently, and "printconfig --all" writes and then removes a fragment in /run/bootc/install that "install config" would pick up while scanning the same directory. Besides the vanishing-file error fixed in the previous commit, the reader could also see the fragment half-written, so take a lock rather than relying on timing. Generated-by: AI Signed-off-by: Colin Walters --- crates/tests-integration/src/container.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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)?;