diff --git a/crates/lib/src/bootc_composefs/backwards_compat/bcompat_boot.rs b/crates/lib/src/bootc_composefs/backwards_compat/bcompat_boot.rs index bf203c0d4..383c47d07 100644 --- a/crates/lib/src/bootc_composefs/backwards_compat/bcompat_boot.rs +++ b/crates/lib/src/bootc_composefs/backwards_compat/bcompat_boot.rs @@ -13,8 +13,9 @@ use crate::{ }, }, composefs_consts::{ - ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_TYPE, STATE_DIR_RELATIVE, TYPE1_BOOT_DIR_PREFIX, - TYPE1_ENT_PATH_STAGED, UKI_NAME_PREFIX, USER_CFG_STAGED, + BLS_ENTRY_PREFIX, ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_TYPE, STATE_DIR_RELATIVE, + TYPE1_BOOT_DIR_PREFIX, TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED, UKI_NAME_PREFIX, + USER_CFG_STAGED, }, parsers::bls_config::{BLSConfig, BLSConfigType, EFIKey}, spec::BootloaderKind, @@ -244,6 +245,17 @@ fn create_staged_bls_entries(boot_dir: &Dir, entries: &Vec<(String, BLSConfig)>) staged_entries.atomic_write(filename, new_entry.to_string().as_bytes())?; } + let original_entries = boot_dir.open_dir(TYPE1_ENT_PATH)?; + for entry in original_entries.entries_utf8()? { + let entry = entry?; + let entry_name = entry.file_name()?; + if entry.file_type()?.is_file() && !entry_name.starts_with(BLS_ENTRY_PREFIX) { + original_entries + .copy(entry_name.clone(), &staged_entries, entry_name) + .context("Staging non-managed BLS entry")?; + } + } + fsync(staged_entries.reopen_as_ownedfd()?).context("fsync") } @@ -396,3 +408,67 @@ pub(crate) async fn prepend_custom_prefix( Ok(()) } + +#[cfg(test)] +mod tests { + use crate::bootc_composefs::backwards_compat::bcompat_boot::*; + use crate::parsers::bls_config::parse_bls_config; + use anyhow::Result; + use cap_std_ext::{cap_std, dirext::CapStdExtDirExt}; + + #[test] + fn test_create_staged_bls_entries() -> Result<()> { + let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?; + + let entry1 = r#" + title Fedora 42.20250623.3.1 (CoreOS) + version fedora-42.0 + sort-key 1 + linux /boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10 + initrd /boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img + options root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6 + "#; + let entry2 = r#" + title Fedora 41.20250214.2.0 (CoreOS) + version fedora-42.0 + sort-key 2 + efi /EFI/Linux/f7415d75017a12a387a39d2281e033a288fc15775108250ef70a01dcadb93346.efi + options root=UUID=abc123 rw composefs=febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01 + "#; + let entry3 = r#" + title Test + version 1 + sort-key 9 + efi /EFI/boot/test.efi + "#; + + tempdir.create_dir_all("loader/entries")?; + tempdir.atomic_write("loader/entries/bootc_entry1.conf", entry1)?; + tempdir.atomic_write("loader/entries/bootc_entry2.conf", entry2)?; + tempdir.atomic_write("loader/entries/entry3.conf", entry3)?; + + let entries = vec![ + ( + "bootc_entry1.conf".to_string(), + parse_bls_config(entry1).unwrap(), + ), + ( + "bootc_entry2.conf".to_string(), + parse_bls_config(entry2).unwrap(), + ), + ]; + + create_staged_bls_entries(&tempdir, &entries).unwrap(); + + assert!(tempdir.exists("loader/entries.staged/bootc_entry1.conf")); + assert!(tempdir.exists("loader/entries.staged/bootc_entry2.conf")); + assert!(tempdir.exists("loader/entries.staged/entry3.conf")); + + assert_eq!( + str::from_utf8(&tempdir.read("loader/entries.staged/entry3.conf")?[..])?, + entry3 + ); + + Ok(()) + } +} diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index dd8c15e74..0125ac617 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -96,7 +96,9 @@ use serde::{Deserialize, Serialize}; use crate::bootc_composefs::state::{get_booted_bls, write_composefs_state}; use crate::bootc_composefs::status::ComposefsCmdline; use crate::bootc_kargs::compute_new_kargs; -use crate::composefs_consts::{TYPE1_BOOT_DIR_PREFIX, TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED}; +use crate::composefs_consts::{ + BLS_ENTRY_PREFIX, TYPE1_BOOT_DIR_PREFIX, TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED, +}; use crate::parsers::bls_config::{BLSConfig, BLSConfigType, EFIKey}; use crate::spec::BootloaderKind; use crate::task::Task; @@ -1065,6 +1067,19 @@ fn write_systemd_uki_config( )?; } + if let BootSetupType::Upgrade(_) = setup_type { + let original_entries = esp_dir.open_dir(TYPE1_ENT_PATH)?; + for entry in original_entries.entries_utf8()? { + let entry = entry?; + let entry_name = entry.file_name()?; + if entry.file_type()?.is_file() && !entry_name.starts_with(BLS_ENTRY_PREFIX) { + original_entries + .copy(entry_name.clone(), &entries_dir, entry_name) + .context("Staging non-managed BLS entry")?; + } + } + } + // Write the timeout for bootloader menu if not exists if !esp_dir.exists(SYSTEMD_LOADER_CONF_PATH) { esp_dir @@ -1490,6 +1505,13 @@ pub(crate) async fn setup_composefs_boot( #[cfg(test)] mod tests { + use cap_std::fs::Dir; + use cap_std_ext::cap_std; + use ostree_ext::{ostree::SysrootBuilder, sysroot::SysrootLock}; + use std::fs::File; + + use crate::{bootc_composefs::digest::new_temp_composefs_repo, k8sapitypes::Resource}; + use super::*; #[test] @@ -1603,4 +1625,98 @@ mod tests { "RHEL should sort before Fedora in descending order" ); } + + #[tokio::test] + async fn test_write_systemd_uki_config() -> Result<()> { + let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?; + + let entry1 = r#" + title Fedora 42.20250623.3.1 (CoreOS) + version fedora-42.0 + sort-key 1 + linux /boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10 + initrd /boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img + options root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6 + "#; + let entry2 = r#" + title Fedora 41.20250214.2.0 (CoreOS) + version fedora-42.0 + sort-key 2 + efi /EFI/Linux/f7415d75017a12a387a39d2281e033a288fc15775108250ef70a01dcadb93346.efi + options root=UUID=abc123 rw composefs=febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01 + "#; + let entry3 = r#" + title Test + version 1 + sort-key 9 + efi /EFI/boot/test.efi + "#; + + tempdir.create_dir_all("loader/entries")?; + tempdir.atomic_write("loader/entries/bootc_entry1.conf", entry1)?; + tempdir.atomic_write("loader/entries/bootc_entry2.conf", entry2)?; + tempdir.atomic_write("loader/entries/entry3.conf", entry3)?; + + let sysroot_path = format!( + "{}/test_write_systemd_uki_config", + std::env::temp_dir().display() + ); + std::fs::create_dir_all(&sysroot_path)?; + let dir = Dir::from_std_file(File::open(&sysroot_path)?); + let sysroot_dir = cap_std_ext::cap_tempfile::tempdir_in(&dir)?; + sysroot_dir.create_dir_all("ostree/repo")?; + sysroot_dir.atomic_write( + "ostree/repo/config", + r#"[core] + repo_version=1 + mode=bare-split-xattrs + "#, + )?; + + let sysroot = SysrootBuilder::new() + .path(Some(sysroot_path.into())) + .create(None)?; + let sysroot_lock = SysrootLock::new_from_sysroot(&sysroot).await?; + let storage = Storage::new_ostree(sysroot_lock, &sysroot_dir)?; + let cmdline = Box::new(ComposefsCmdline::new(Default::default())); + let (_repo_dir, repo) = new_temp_composefs_repo()?; + let booted_composefs = BootedComposefs { + repo: repo, + cmdline: Box::leak(cmdline), + }; + let host = Host { + resource: Resource { + api_version: Default::default(), + kind: Default::default(), + metadata: Default::default(), + }, + spec: Default::default(), + status: Default::default(), + }; + let boot_setup_type = BootSetupType::Upgrade((&storage, &booted_composefs, &host)); + let boot_label = UKIInfo { + boot_label: "label".to_string(), + version: None, + os_id: None, + boot_digest: "".to_string(), + }; + let id = Sha512HashValue::EMPTY; + let bootloader = Bootloader::Systemd; + + write_systemd_uki_config(&tempdir, &boot_setup_type, boot_label, &id, &bootloader)?; + + assert!(tempdir.exists(SYSTEMD_LOADER_CONF_PATH)); + assert!(tempdir.exists("loader/entries.staged")); + assert!(tempdir.exists("loader/entries.staged/bootc_bootc-00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000-1.conf")); + assert!(tempdir.exists("loader/entries.staged/bootc_bootc-fedora-42.0-0.conf")); + assert!(!tempdir.exists("loader/entries.staged/bootc_bootc-fedora-42.0-1.conf")); + assert!(tempdir.exists("loader/entries.staged/entry3.conf")); + + assert_eq!( + str::from_utf8(&tempdir.read("loader/entries.staged/entry3.conf")?[..])?, + entry3 + ); + + Ok(()) + } } diff --git a/crates/lib/src/bootc_composefs/rollback.rs b/crates/lib/src/bootc_composefs/rollback.rs index 307545463..346f11324 100644 --- a/crates/lib/src/bootc_composefs/rollback.rs +++ b/crates/lib/src/bootc_composefs/rollback.rs @@ -13,7 +13,8 @@ use crate::bootc_composefs::boot::{ }; use crate::bootc_composefs::status::{get_composefs_status, get_sorted_type1_boot_entries}; use crate::composefs_consts::{ - COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR, TYPE1_ENT_PATH_STAGED, + BLS_ENTRY_PREFIX, COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR, + TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED, }; use crate::deploy::ROLLBACK_JOURNAL_ID; use crate::spec::{Bootloader, BootloaderKind, Host}; @@ -198,6 +199,17 @@ fn rollback_composefs_entries(host: &Host, boot_dir: &Dir, bootloader: Bootloade .with_context(|| format!("Writing to {file_name}"))?; } + let original_entries = boot_dir.open_dir(TYPE1_ENT_PATH)?; + for entry in original_entries.entries_utf8()? { + let entry = entry?; + let entry_name = entry.file_name()?; + if entry.file_type()?.is_file() && !entry_name.starts_with(BLS_ENTRY_PREFIX) { + original_entries + .copy(entry_name.clone(), &rollback_entries_dir, entry_name) + .context("Staging non-managed BLS entry")?; + } + } + let rollback_entries_dir = rollback_entries_dir .reopen_as_ownedfd() .context("Reopening as owned fd")?; @@ -276,3 +288,70 @@ pub(crate) async fn composefs_rollback( Ok(()) } + +#[cfg(test)] +mod tests { + use anyhow::{Ok, Result}; + use cap_std_ext::{cap_std, dirext::CapStdExtDirExt}; + + use crate::k8sapitypes::Resource; + use crate::spec::Bootloader; + use crate::{bootc_composefs::rollback::rollback_composefs_entries, spec::Host}; + + #[test] + fn test_rollback_composefs_entries() -> Result<()> { + let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?; + + let entry1 = r#" + title Fedora 42.20250623.3.1 (CoreOS) + version fedora-42.0 + sort-key 1 + linux /boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10 + initrd /boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img + options root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6 + "#; + let entry2 = r#" + title Fedora 41.20250214.2.0 (CoreOS) + version fedora-42.0 + sort-key 2 + efi /EFI/Linux/f7415d75017a12a387a39d2281e033a288fc15775108250ef70a01dcadb93346.efi + options root=UUID=abc123 rw composefs=febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01 + "#; + let entry3 = r#" + title Test + version 1 + sort-key 9 + efi /EFI/boot/test.efi + "#; + + tempdir.create_dir_all("loader/entries")?; + tempdir.atomic_write("loader/entries/bootc_entry1.conf", entry1)?; + tempdir.atomic_write("loader/entries/bootc_entry2.conf", entry2)?; + tempdir.atomic_write("loader/entries/entry3.conf", entry3)?; + + let host = Host { + resource: Resource { + api_version: Default::default(), + kind: Default::default(), + metadata: Default::default(), + }, + spec: Default::default(), + status: Default::default(), + }; + let bootloader = Bootloader::None; + + rollback_composefs_entries(&host, &tempdir, bootloader)?; + + assert!(tempdir.exists("loader/entries")); + assert!(tempdir.exists("loader/entries/bootc_bootc-fedora-42.0-0.conf")); + assert!(tempdir.exists("loader/entries/bootc_bootc-fedora-42.0-1.conf")); + assert!(tempdir.exists("loader/entries/entry3.conf")); + + assert_eq!( + str::from_utf8(&tempdir.read("loader/entries/entry3.conf")?[..])?, + entry3 + ); + + Ok(()) + } +} diff --git a/crates/lib/src/bootc_composefs/status.rs b/crates/lib/src/bootc_composefs/status.rs index c2a984e79..59439e678 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -18,8 +18,9 @@ use crate::{ utils::{compute_store_boot_digest_for_uki, get_uki_cmdline}, }, composefs_consts::{ - COMPOSEFS_CMDLINE, ORIGIN_KEY_BOOT_DIGEST, ORIGIN_KEY_IMAGE, ORIGIN_KEY_MANIFEST_DIGEST, - TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED, USER_CFG, USER_CFG_STAGED, + BLS_ENTRY_PREFIX, COMPOSEFS_CMDLINE, ORIGIN_KEY_BOOT_DIGEST, ORIGIN_KEY_IMAGE, + ORIGIN_KEY_MANIFEST_DIGEST, TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED, USER_CFG, + USER_CFG_STAGED, }, install::EFI_LOADER_INFO, parsers::{ @@ -305,7 +306,7 @@ fn get_sorted_type1_boot_entries_helper( .to_str() .ok_or(anyhow::anyhow!("Found non UTF-8 characters in filename"))?; - if !file_name.ends_with(".conf") { + if !(file_name.starts_with(BLS_ENTRY_PREFIX) && file_name.ends_with(".conf")) { continue; } @@ -1116,8 +1117,16 @@ mod tests { "loader/entries/random_file.txt", "Random file that we won't parse", )?; - tempdir.atomic_write("loader/entries/entry1.conf", entry1)?; - tempdir.atomic_write("loader/entries/entry2.conf", entry2)?; + tempdir.atomic_write( + "loader/entries/random_file.conf", + "Random file that we won't parse", + )?; + tempdir.atomic_write( + "loader/entries/bootc_random_file.txt", + "Random file that we won't parse", + )?; + tempdir.atomic_write("loader/entries/bootc_entry1.conf", entry1)?; + tempdir.atomic_write("loader/entries/bootc_entry2.conf", entry2)?; let result = get_sorted_type1_boot_entries_helper(&tempdir, true, false, Bootloader::Systemd) @@ -1279,8 +1288,8 @@ mod tests { tempdir.create_dir_all("loader/entries")?; tempdir.create_dir_all("loader/entries.staged")?; - tempdir.atomic_write("loader/entries/active.conf", active_entry)?; - tempdir.atomic_write("loader/entries.staged/staged.conf", staged_entry)?; + tempdir.atomic_write("loader/entries/bootc_active.conf", active_entry)?; + tempdir.atomic_write("loader/entries.staged/bootc_staged.conf", staged_entry)?; let result = list_type1_entries(&tempdir)?; assert_eq!(result.len(), 2); @@ -1317,7 +1326,7 @@ mod tests { let entry2 = format!( r#" - title Fedora Linux (2.0.0) + title Fedora Linux (2.0.0) version 2.0.0 sort-key {} linux /boot/vmlinuz diff --git a/crates/lib/src/composefs_consts.rs b/crates/lib/src/composefs_consts.rs index 8617f1005..d53255572 100644 --- a/crates/lib/src/composefs_consts.rs +++ b/crates/lib/src/composefs_consts.rs @@ -48,6 +48,9 @@ pub(crate) const TYPE1_BOOT_DIR_PREFIX: &str = "bootc_composefs-"; /// The prefix for names of UKI and UKI Addons pub(crate) const UKI_NAME_PREFIX: &str = TYPE1_BOOT_DIR_PREFIX; +/// The prefix for BLS file entries +pub(crate) const BLS_ENTRY_PREFIX: &str = "bootc_"; + /// Prefix for OCI tags owned by bootc in the composefs repository. /// /// Tags are created as `localhost/bootc-` to act as GC roots diff --git a/crates/lib/src/parsers/bls_config.rs b/crates/lib/src/parsers/bls_config.rs index c796ffdab..38ac6ad06 100644 --- a/crates/lib/src/parsers/bls_config.rs +++ b/crates/lib/src/parsers/bls_config.rs @@ -868,6 +868,34 @@ mod tests { Ok(()) } + #[test] + fn test_boot_artifact_verity_efi_no_prefix() -> Result<()> { + let input = r#" + title Test + version 1 + efi /EFI/boot/test.efi + "#; + let config = parse_bls_config(&input)?; + + let result = config.get_verity(); + assert!(result.is_err()); + Ok(()) + } + + #[test] + fn test_boot_artifact_verity_efi_no_extension() -> Result<()> { + let input = r#" + title Test + version 1 + efi /EFI/boot/test + "#; + let config = parse_bls_config(&input)?; + + let result = config.get_verity(); + assert!(result.is_err()); + Ok(()) + } + /// Test that Non-EFI boot_artifact_name fails when linux path has no parent #[test] fn test_boot_artifact_name_non_efi_no_parent() { diff --git a/tmt/tests/booted/test-composefs-corruped-state-resilience.nu b/tmt/tests/booted/test-composefs-corruped-state-resilience.nu index cbd10f67b..943f2a681 100644 --- a/tmt/tests/booted/test-composefs-corruped-state-resilience.nu +++ b/tmt/tests/booted/test-composefs-corruped-state-resilience.nu @@ -41,9 +41,9 @@ def first_boot [] { systemd-run -p MountFlags=slave -qdPG -- /bin/sh -c $" mount -orw,remount /sysroot cd ($entries_dir) - cp * new-entry.conf - - sed -i 's;($booted_verity);bad-verity;' new-entry.conf + cp * bootc_new-entry.conf + + sed -i 's;($booted_verity);bad-verity;' bootc_new-entry.conf " # This should work but log a warning in journal @@ -76,8 +76,8 @@ def first_boot [] { # Test the same thing but with an existing rollback deployment def second_boot [] { assert equal $booted.image.image "localhost/bootc-test-1" - - # Create another derived image + + # Create another derived image tap make_uki_containerfile $" FROM localhost/bootc as base RUN echo 'second-deployment' > /usr/share/deployment-marker