From ea4253bfa03298cf817b07cfd0c4c02dae72d0ee Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 19 Nov 2025 14:42:32 +0530 Subject: [PATCH 1/5] composefs/bls: Get cmdline from usr/lib/bootc/kargs.d Parse toml files in usr/lib/bootc/kargs.d and append them to kernel cmdline on install and upgrade/switch. Also, copy over current deployment's cmdline args on upgrade/switch to another deployment Signed-off-by: Pragyan Poudyal --- crates/lib/src/bootc_composefs/boot.rs | 45 ++++++++++++++++++++++--- crates/lib/src/bootc_kargs.rs | 46 ++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index 5cc475b75..f2ed34957 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -5,7 +5,7 @@ use std::path::Path; use anyhow::{anyhow, Context, Result}; use bootc_blockdev::find_parent_devices; -use bootc_kernel_cmdline::utf8::Cmdline; +use bootc_kernel_cmdline::utf8::{Cmdline, Parameter}; use bootc_mount::inspect_filesystem_of_dir; use bootc_mount::tempmount::TempMount; use camino::{Utf8Path, Utf8PathBuf}; @@ -33,6 +33,7 @@ use rustix::{mount::MountFlags, path::Arg}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use crate::bootc_kargs::kargs_from_composefs_filesystem; use crate::composefs_consts::{TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED}; use crate::parsers::bls_config::{BLSConfig, BLSConfigType}; use crate::parsers::grub_menuconfig::MenuEntry; @@ -51,7 +52,6 @@ use crate::{ BOOT_LOADER_ENTRIES, COMPOSEFS_CMDLINE, ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_DIGEST, STAGED_BOOT_LOADER_ENTRIES, STATE_DIR_ABS, USER_CFG, USER_CFG_STAGED, }, - install::RW_KARG, spec::{Bootloader, Host}, }; @@ -384,7 +384,7 @@ pub(crate) fn setup_composefs_bls_boot( ) -> Result { let id_hex = id.to_hex(); - let (root_path, esp_device, cmdline_refs, fs, bootloader) = match setup_type { + let (root_path, esp_device, mut cmdline_refs, fs, bootloader) = match setup_type { BootSetupType::Setup((root_setup, state, postfetch, fs)) => { // root_setup.kargs has [root=UUID=, "rw"] let mut cmdline_options = Cmdline::new(); @@ -415,16 +415,53 @@ pub(crate) fn setup_composefs_bls_boot( let sysroot_parent = get_sysroot_parent_dev(&storage.physical_root)?; let bootloader = host.require_composefs_booted()?.bootloader.clone(); + let current_cfg = match bootloader { + Bootloader::Grub => { + let boot_dir = storage + .physical_root + .open_dir("boot") + .context("Opening boot")?; + + get_booted_bls(&boot_dir)? + } + + Bootloader::Systemd => { + let esp = get_esp_partition(&sysroot_parent)?.0; + let esp_mnt = mount_esp(&esp)?; + + get_booted_bls(&esp_mnt.fd)? + } + }; + + let mut cmdline = match current_cfg.cfg_type { + BLSConfigType::NonEFI { options, .. } => { + let options = options + .ok_or_else(|| anyhow::anyhow!("No 'options' found in BLS Config"))?; + + Cmdline::from(options) + } + + _ => anyhow::bail!("Found NonEFI config"), + }; + + // Copy all cmdline args, replacing only `composefs=` + let param = format!("{COMPOSEFS_CMDLINE}={id_hex}"); + let param = + Parameter::parse(¶m).context("Failed to create 'composefs=' parameter")?; + cmdline.add_or_modify(¶m); + ( Utf8PathBuf::from("/sysroot"), get_esp_partition(&sysroot_parent)?.0, - Cmdline::from(format!("{RW_KARG} {COMPOSEFS_CMDLINE}={id_hex}")), + cmdline, fs, bootloader, ) } }; + kargs_from_composefs_filesystem(fs, &repo, &mut cmdline_refs)?; + let is_upgrade = matches!(setup_type, BootSetupType::Upgrade(..)); let (entry_paths, _tmpdir_guard) = match bootloader { diff --git a/crates/lib/src/bootc_kargs.rs b/crates/lib/src/bootc_kargs.rs index af709f1af..88b532697 100644 --- a/crates/lib/src/bootc_kargs.rs +++ b/crates/lib/src/bootc_kargs.rs @@ -6,6 +6,8 @@ use cap_std_ext::cap_std::fs::Dir; use cap_std_ext::cap_std::fs_utf8::Dir as DirUtf8; use cap_std_ext::dirext::CapStdExtDirExt; use cap_std_ext::dirext::CapStdExtDirExtUtf8; +use composefs::fs::read_file; +use composefs::generic_tree::Inode; use ostree::gio; use ostree_ext::ostree; use ostree_ext::ostree::Deployment; @@ -13,9 +15,11 @@ use ostree_ext::prelude::Cast; use ostree_ext::prelude::FileEnumeratorExt; use ostree_ext::prelude::FileExt; use serde::Deserialize; +use std::ffi::OsStr; use crate::deploy::ImageState; use crate::store::Storage; +use crate::store::{ComposefsFilesystem, ComposefsRepository}; /// The relative path to the kernel arguments which may be embedded in an image. const KARGS_PATH: &str = "usr/lib/bootc/kargs.d"; @@ -45,6 +49,48 @@ impl Config { } } +/// Looks for files in usr/lib/bootc/kargs.d and parses cmdline agruments +pub(crate) fn kargs_from_composefs_filesystem( + fs: &ComposefsFilesystem, + repo: &ComposefsRepository, + cmdline: &mut Cmdline, +) -> Result<()> { + let kargs_d = fs + .root + .get_directory_opt(OsStr::new(KARGS_PATH)) + .with_context(|| format!("Getting {KARGS_PATH}"))?; + + let Some(kargs_d) = kargs_d else { + return Ok(()); + }; + + for (fname, inode) in kargs_d.sorted_entries() { + let Inode::Leaf(..) = inode else { continue }; + + let fname_str = fname + .to_str() + .ok_or_else(|| anyhow::anyhow!("Failed to get filename as string"))?; + + if !Config::filename_matches(fname_str) { + continue; + } + + let file = kargs_d + .get_file(fname) + .with_context(|| format!("Getting file {fname_str}"))?; + + let contents = read_file(&file, &repo)?; + let contents = std::str::from_utf8(&contents) + .with_context(|| format!("File {fname_str} is not a valid UTF-8"))?; + + if let Some(kargs) = parse_kargs_toml(contents, std::env::consts::ARCH)? { + cmdline.extend(&kargs); + }; + } + + Ok(()) +} + /// Load and parse all bootc kargs.d files in the specified root, returning /// a combined list. pub(crate) fn get_kargs_in_root(d: &Dir, sys_arch: &str) -> Result { From 0c9b17f3aac56f163155dee610edcd497aecb25b Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 19 Nov 2025 16:38:34 +0530 Subject: [PATCH 2/5] storage: Add `boot_dir` and `esp` fields We have a lot of places where we mount the ESP temporarily and a lot of switch cases for Grub's vs SystemdBoot's 'boot' directory. We add a `boot_dir` field in Storage which points to `/sysroot/boot` for systems with Grub as the bootloader and points to the ESP for systems with SystemdBoot as the bootloader. Also we mount the ESP temporarily while creating the storage struct, which cleans up the code quite a bit. Signed-off-by: Pragyan Poudyal --- crates/lib/src/bootc_composefs/boot.rs | 19 +-------- crates/lib/src/bootc_composefs/delete.rs | 49 +++++++++------------- crates/lib/src/bootc_composefs/finalize.rs | 35 ++++++---------- crates/lib/src/bootc_composefs/gc.rs | 18 +++----- crates/lib/src/bootc_composefs/rollback.rs | 34 +++++---------- crates/lib/src/bootc_composefs/status.rs | 38 +++++------------ crates/lib/src/store/mod.rs | 40 +++++++++++++++++- 7 files changed, 101 insertions(+), 132 deletions(-) diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index f2ed34957..10eb67957 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -415,23 +415,8 @@ pub(crate) fn setup_composefs_bls_boot( let sysroot_parent = get_sysroot_parent_dev(&storage.physical_root)?; let bootloader = host.require_composefs_booted()?.bootloader.clone(); - let current_cfg = match bootloader { - Bootloader::Grub => { - let boot_dir = storage - .physical_root - .open_dir("boot") - .context("Opening boot")?; - - get_booted_bls(&boot_dir)? - } - - Bootloader::Systemd => { - let esp = get_esp_partition(&sysroot_parent)?.0; - let esp_mnt = mount_esp(&esp)?; - - get_booted_bls(&esp_mnt.fd)? - } - }; + let boot_dir = storage.require_boot_dir()?; + let current_cfg = get_booted_bls(&boot_dir)?; let mut cmdline = match current_cfg.cfg_type { BLSConfigType::NonEFI { options, .. } => { diff --git a/crates/lib/src/bootc_composefs/delete.rs b/crates/lib/src/bootc_composefs/delete.rs index b16930e83..f1a31101c 100644 --- a/crates/lib/src/bootc_composefs/delete.rs +++ b/crates/lib/src/bootc_composefs/delete.rs @@ -7,10 +7,7 @@ use composefs_boot::bootloader::{EFI_ADDON_DIR_EXT, EFI_EXT}; use crate::{ bootc_composefs::{ - boot::{ - find_vmlinuz_initrd_duplicates, get_efi_uuid_source, get_esp_partition, - get_sysroot_parent_dev, mount_esp, BootType, SYSTEMD_UKI_DIR, - }, + boot::{find_vmlinuz_initrd_duplicates, get_efi_uuid_source, BootType, SYSTEMD_UKI_DIR}, gc::composefs_gc, repo::open_composefs_repo, rollback::{composefs_rollback, rename_exchange_user_cfg}, @@ -215,40 +212,34 @@ fn remove_grub_menucfg_entry(id: &str, boot_dir: &Dir, deleting_staged: bool) -> #[fn_error_context::context("Deleting boot entries for deployment {}", deployment.deployment.verity)] fn delete_depl_boot_entries( deployment: &DeploymentEntry, - physical_root: &Dir, + storage: &Storage, deleting_staged: bool, ) -> Result<()> { - match deployment.deployment.bootloader { - Bootloader::Grub => { - let boot_dir = physical_root.open_dir("boot").context("Opening boot dir")?; + let boot_dir = storage.require_boot_dir()?; - match deployment.deployment.boot_type { - BootType::Bls => delete_type1_entry(deployment, &boot_dir, deleting_staged), + match deployment.deployment.bootloader { + Bootloader::Grub => match deployment.deployment.boot_type { + BootType::Bls => delete_type1_entry(deployment, boot_dir, deleting_staged), - BootType::Uki => { - let device = get_sysroot_parent_dev(physical_root)?; - let (esp_part, ..) = get_esp_partition(&device)?; - let esp_mount = mount_esp(&esp_part)?; + BootType::Uki => { + let esp = storage + .esp + .as_ref() + .ok_or_else(|| anyhow::anyhow!("ESP not found"))?; - remove_grub_menucfg_entry( - &deployment.deployment.verity, - &boot_dir, - deleting_staged, - )?; + remove_grub_menucfg_entry( + &deployment.deployment.verity, + boot_dir, + deleting_staged, + )?; - delete_uki(&deployment.deployment.verity, &esp_mount.fd) - } + delete_uki(&deployment.deployment.verity, &esp.fd) } - } + }, Bootloader::Systemd => { - let device = get_sysroot_parent_dev(physical_root)?; - let (esp_part, ..) = get_esp_partition(&device)?; - - let esp_mount = mount_esp(&esp_part)?; - // For Systemd UKI as well, we use .conf files - delete_type1_entry(deployment, &esp_mount.fd, deleting_staged) + delete_type1_entry(deployment, boot_dir, deleting_staged) } } } @@ -362,7 +353,7 @@ pub(crate) async fn delete_composefs_deployment( tracing::info!("Deleting {kind}deployment '{deployment_id}'"); - delete_depl_boot_entries(&depl_to_del, &storage.physical_root, deleting_staged)?; + delete_depl_boot_entries(&depl_to_del, &storage, deleting_staged)?; composefs_gc(storage, booted_cfs).await?; diff --git a/crates/lib/src/bootc_composefs/finalize.rs b/crates/lib/src/bootc_composefs/finalize.rs index d397c9f5c..027ffb5ee 100644 --- a/crates/lib/src/bootc_composefs/finalize.rs +++ b/crates/lib/src/bootc_composefs/finalize.rs @@ -1,8 +1,6 @@ use std::path::Path; -use crate::bootc_composefs::boot::{ - get_esp_partition, get_sysroot_parent_dev, mount_esp, BootType, -}; +use crate::bootc_composefs::boot::BootType; use crate::bootc_composefs::rollback::{rename_exchange_bls_entries, rename_exchange_user_cfg}; use crate::bootc_composefs::status::get_composefs_status; use crate::composefs_consts::STATE_DIR_ABS; @@ -86,15 +84,12 @@ pub(crate) async fn composefs_backend_finalize( // Unmount EROFS drop(erofs_tmp_mnt); - let sysroot_parent = get_sysroot_parent_dev(&storage.physical_root)?; - // NOTE: Assumption here that ESP will always be present - let (esp_part, ..) = get_esp_partition(&sysroot_parent)?; + let boot_dir = storage.require_boot_dir()?; - let esp_mount = mount_esp(&esp_part)?; - let boot_dir = storage - .physical_root - .open_dir("boot") - .context("Opening boot")?; + let esp_mount = storage + .esp + .as_ref() + .ok_or_else(|| anyhow::anyhow!("ESP not found"))?; // NOTE: Assuming here we won't have two bootloaders at the same time match booted_composefs.bootloader { @@ -103,21 +98,17 @@ pub(crate) async fn composefs_backend_finalize( let entries_dir = boot_dir.open_dir("loader")?; rename_exchange_bls_entries(&entries_dir)?; } - BootType::Uki => finalize_staged_grub_uki(&esp_mount.fd, &boot_dir)?, + BootType::Uki => finalize_staged_grub_uki(&esp_mount.fd, boot_dir)?, }, - Bootloader::Systemd => match staged_composefs.boot_type { - BootType::Bls => { - let entries_dir = esp_mount.fd.open_dir("loader")?; - rename_exchange_bls_entries(&entries_dir)?; - } - BootType::Uki => { + Bootloader::Systemd => { + if matches!(staged_composefs.boot_type, BootType::Uki) { rename_staged_uki_entries(&esp_mount.fd)?; - - let entries_dir = esp_mount.fd.open_dir("loader")?; - rename_exchange_bls_entries(&entries_dir)?; } - }, + + let entries_dir = boot_dir.open_dir("loader")?; + rename_exchange_bls_entries(&entries_dir)?; + } }; Ok(()) diff --git a/crates/lib/src/bootc_composefs/gc.rs b/crates/lib/src/bootc_composefs/gc.rs index 8195cade2..7926d250c 100644 --- a/crates/lib/src/bootc_composefs/gc.rs +++ b/crates/lib/src/bootc_composefs/gc.rs @@ -10,7 +10,6 @@ use composefs::fsverity::{FsVerityHashValue, Sha512HashValue}; use crate::{ bootc_composefs::{ - boot::{get_esp_partition, get_sysroot_parent_dev, mount_esp}, delete::{delete_image, delete_staged, delete_state_dir, get_image_objects}, status::{ get_bootloader, get_composefs_status, get_sorted_grub_uki_boot_entries, @@ -44,20 +43,19 @@ fn list_erofs_images(sysroot: &Dir) -> Result> { /// # Returns /// The fsverity of EROFS images corresponding to boot entries #[fn_error_context::context("Listing bootloader entries")] -fn list_bootloader_entries(physical_root: &Dir) -> Result> { +fn list_bootloader_entries(storage: &Storage) -> Result> { let bootloader = get_bootloader()?; + let boot_dir = storage.require_boot_dir()?; let entries = match bootloader { Bootloader::Grub => { - let boot_dir = physical_root.open_dir("boot").context("Opening boot dir")?; - // Grub entries are always in boot let grub_dir = boot_dir.open_dir("grub2").context("Opening grub dir")?; if grub_dir.exists(USER_CFG) { // Grub UKI let mut s = String::new(); - let boot_entries = get_sorted_grub_uki_boot_entries(&boot_dir, &mut s)?; + let boot_entries = get_sorted_grub_uki_boot_entries(boot_dir, &mut s)?; boot_entries .into_iter() @@ -65,7 +63,7 @@ fn list_bootloader_entries(physical_root: &Dir) -> Result> { .collect::, _>>()? } else { // Type1 Entry - let boot_entries = get_sorted_type1_boot_entries(&boot_dir, true)?; + let boot_entries = get_sorted_type1_boot_entries(boot_dir, true)?; boot_entries .into_iter() @@ -75,11 +73,7 @@ fn list_bootloader_entries(physical_root: &Dir) -> Result> { } Bootloader::Systemd => { - let device = get_sysroot_parent_dev(physical_root)?; - let (esp_part, ..) = get_esp_partition(&device)?; - let esp_mount = mount_esp(&esp_part)?; - - let boot_entries = get_sorted_type1_boot_entries(&esp_mount.fd, true)?; + let boot_entries = get_sorted_type1_boot_entries(boot_dir, true)?; boot_entries .into_iter() @@ -175,7 +169,7 @@ pub(crate) async fn composefs_gc(storage: &Storage, booted_cfs: &BootedComposefs let sysroot = &storage.physical_root; - let bootloader_entries = list_bootloader_entries(&storage.physical_root)?; + let bootloader_entries = list_bootloader_entries(&storage)?; let images = list_erofs_images(&sysroot)?; // Collect the deployments that have an image but no bootloader entry diff --git a/crates/lib/src/bootc_composefs/rollback.rs b/crates/lib/src/bootc_composefs/rollback.rs index 6338bf9b5..5cfd60c39 100644 --- a/crates/lib/src/bootc_composefs/rollback.rs +++ b/crates/lib/src/bootc_composefs/rollback.rs @@ -6,9 +6,7 @@ use cap_std_ext::dirext::CapStdExtDirExt; use fn_error_context::context; use rustix::fs::{fsync, renameat_with, AtFlags, RenameFlags}; -use crate::bootc_composefs::boot::{ - get_esp_partition, get_sysroot_parent_dev, mount_esp, type1_entry_conf_file_name, BootType, -}; +use crate::bootc_composefs::boot::{type1_entry_conf_file_name, BootType}; use crate::bootc_composefs::status::{get_composefs_status, get_sorted_type1_boot_entries}; use crate::composefs_consts::TYPE1_ENT_PATH_STAGED; use crate::spec::Bootloader; @@ -196,31 +194,21 @@ pub(crate) async fn composefs_rollback( anyhow::bail!("Rollback deployment not a composefs deployment") }; + let boot_dir = storage.require_boot_dir()?; + match &rollback_entry.bootloader { - Bootloader::Grub => { - let boot_dir = storage - .physical_root - .open_dir("boot") - .context("Opening boot dir")?; - - match rollback_entry.boot_type { - BootType::Bls => { - rollback_composefs_entries(&boot_dir, rollback_entry.bootloader.clone())?; - } - - BootType::Uki => { - rollback_grub_uki_entries(&boot_dir)?; - } + Bootloader::Grub => match rollback_entry.boot_type { + BootType::Bls => { + rollback_composefs_entries(boot_dir, rollback_entry.bootloader.clone())?; } - } + BootType::Uki => { + rollback_grub_uki_entries(boot_dir)?; + } + }, Bootloader::Systemd => { - let parent = get_sysroot_parent_dev(&storage.physical_root)?; - let (esp_part, ..) = get_esp_partition(&parent)?; - let esp_mount = mount_esp(&esp_part)?; - // We use BLS entries for systemd UKI as well - rollback_composefs_entries(&esp_mount.fd, rollback_entry.bootloader.clone())?; + rollback_composefs_entries(boot_dir, rollback_entry.bootloader.clone())?; } } diff --git a/crates/lib/src/bootc_composefs/status.rs b/crates/lib/src/bootc_composefs/status.rs index 29ae212ba..40ec1b757 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -5,7 +5,7 @@ use bootc_kernel_cmdline::utf8::Cmdline; use fn_error_context::context; use crate::{ - bootc_composefs::boot::{get_esp_partition, get_sysroot_parent_dev, mount_esp, BootType}, + bootc_composefs::boot::BootType, composefs_consts::{COMPOSEFS_CMDLINE, ORIGIN_KEY_BOOT_DIGEST, TYPE1_ENT_PATH, USER_CFG}, install::EFI_LOADER_INFO, parsers::{ @@ -13,6 +13,7 @@ use crate::{ grub_menuconfig::{parse_grub_menuentry_file, MenuEntry}, }, spec::{BootEntry, BootOrder, Host, HostSpec, ImageReference, ImageStatus}, + store::Storage, utils::{read_uefi_var, EfiError}, }; @@ -254,17 +255,20 @@ pub(crate) async fn get_composefs_status( storage: &crate::store::Storage, booted_cfs: &crate::store::BootedComposefs, ) -> Result { - composefs_deployment_status_from(&storage.physical_root, booted_cfs.cmdline).await + composefs_deployment_status_from(&storage, booted_cfs.cmdline).await } #[context("Getting composefs deployment status")] pub(crate) async fn composefs_deployment_status_from( - sysroot: &Dir, + storage: &Storage, cmdline: &ComposefsCmdline, ) -> Result { let composefs_digest = &cmdline.digest; - let deployments = sysroot + let boot_dir = storage.require_boot_dir()?; + + let deployments = storage + .physical_root .read_dir(STATE_DIR_RELATIVE) .with_context(|| format!("Reading sysroot {STATE_DIR_RELATIVE}"))?; @@ -348,30 +352,10 @@ pub(crate) async fn composefs_deployment_status_from( let booted = host.require_composefs_booted()?; - let (boot_dir, _temp_guard) = match booted.bootloader { - Bootloader::Grub => (sysroot.open_dir("boot").context("Opening boot dir")?, None), - - // TODO: This is redundant as we should already have ESP mounted at `/efi/` accoding to - // spec; currently we do not - // - // See: https://uapi-group.org/specifications/specs/boot_loader_specification/#mount-points - Bootloader::Systemd => { - let parent = get_sysroot_parent_dev(sysroot)?; - let (esp_part, ..) = get_esp_partition(&parent)?; - - let esp_mount = mount_esp(&esp_part)?; - - let dir = esp_mount.fd.try_clone().context("Cloning fd")?; - let guard = Some(esp_mount); - - (dir, guard) - } - }; - let is_rollback_queued = match booted.bootloader { Bootloader::Grub => match boot_type { BootType::Bls => { - let bls_config = get_sorted_type1_boot_entries(&boot_dir, false)?; + let bls_config = get_sorted_type1_boot_entries(boot_dir, false)?; let bls_config = bls_config .first() .ok_or(anyhow::anyhow!("First boot entry not found"))?; @@ -392,7 +376,7 @@ pub(crate) async fn composefs_deployment_status_from( BootType::Uki => { let mut s = String::new(); - !get_sorted_grub_uki_boot_entries(&boot_dir, &mut s)? + !get_sorted_grub_uki_boot_entries(boot_dir, &mut s)? .first() .ok_or(anyhow::anyhow!("First boot entry not found"))? .body @@ -403,7 +387,7 @@ pub(crate) async fn composefs_deployment_status_from( // We will have BLS stuff and the UKI stuff in the same DIR Bootloader::Systemd => { - let bls_config = get_sorted_type1_boot_entries(&boot_dir, false)?; + let bls_config = get_sorted_type1_boot_entries(boot_dir, false)?; let bls_config = bls_config .first() .ok_or(anyhow::anyhow!("First boot entry not found"))?; diff --git a/crates/lib/src/store/mod.rs b/crates/lib/src/store/mod.rs index eb65e6eb9..4779093c5 100644 --- a/crates/lib/src/store/mod.rs +++ b/crates/lib/src/store/mod.rs @@ -21,6 +21,7 @@ use std::ops::Deref; use std::sync::Arc; use anyhow::{Context, Result}; +use bootc_mount::tempmount::TempMount; use cap_std_ext::cap_std; use cap_std_ext::cap_std::fs::{Dir, DirBuilder, DirBuilderExt as _}; use cap_std_ext::dirext::CapStdExtDirExt; @@ -31,10 +32,11 @@ use ostree_ext::sysroot::SysrootLock; use ostree_ext::{gio, ostree}; use rustix::fs::Mode; -use crate::bootc_composefs::status::{composefs_booted, ComposefsCmdline}; +use crate::bootc_composefs::boot::{get_esp_partition, get_sysroot_parent_dev, mount_esp}; +use crate::bootc_composefs::status::{composefs_booted, get_bootloader, ComposefsCmdline}; use crate::lsm; use crate::podstorage::CStorage; -use crate::spec::ImageStatus; +use crate::spec::{Bootloader, ImageStatus}; use crate::utils::{deployment_fd, open_dir_remount_rw}; /// See https://github.com/containers/composefs-rs/issues/159 @@ -169,9 +171,23 @@ impl BootedStorage { } let composefs = Arc::new(composefs); + // NOTE: This is assuming that we'll only have composefs in a UEFI system + // We do have this assumptions in a lot of other places + let parent = get_sysroot_parent_dev(&physical_root)?; + let (esp_part, ..) = get_esp_partition(&parent)?; + let esp_mount = mount_esp(&esp_part)?; + + let boot_dir = match get_bootloader()? { + Bootloader::Grub => physical_root.open_dir("boot").context("Opening boot")?, + // NOTE: Handle XBOOTLDR partitions here if and when we use it + Bootloader::Systemd => esp_mount.fd.try_clone().context("Cloning fd")?, + }; + let storage = Storage { physical_root, run, + boot_dir: Some(boot_dir), + esp: Some(esp_mount), ostree: Default::default(), composefs: OnceCell::from(composefs), imgstore: Default::default(), @@ -194,6 +210,8 @@ impl BootedStorage { let storage = Storage { physical_root, run, + boot_dir: None, + esp: None, ostree: OnceCell::from(sysroot), composefs: Default::default(), imgstore: Default::default(), @@ -235,6 +253,15 @@ impl BootedStorage { pub(crate) struct Storage { /// Directory holding the physical root pub physical_root: Dir, + + /// The 'boot' directory, useful and `Some` only for composefs systems + /// For grub booted systems, this points to `/sysroot/boot` + /// For systemd booted systems, this points to the ESP + pub boot_dir: Option, + + /// The ESP mounted at a tmp location + pub esp: Option, + /// Our runtime state run: Dir, @@ -286,12 +313,21 @@ impl Storage { Ok(Self { physical_root, run, + boot_dir: None, + esp: None, ostree: ostree_cell, composefs: Default::default(), imgstore: Default::default(), }) } + /// Returns `boot_dir` if it exists + pub(crate) fn require_boot_dir(&self) -> Result<&Dir> { + self.boot_dir + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Boot dir not found")) + } + /// Access the underlying ostree repository pub(crate) fn get_ostree(&self) -> Result<&SysrootLock> { self.ostree From 20eb4e46b0463197fc2b234868a529cf6ef2b2d3 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Thu, 20 Nov 2025 11:43:17 +0530 Subject: [PATCH 3/5] kargs: Handle addition/removal of kargs from kargs.d Signed-off-by: Pragyan Poudyal --- crates/lib/src/bootc_composefs/boot.rs | 4 +- crates/lib/src/bootc_kargs.rs | 83 +++++++++++++++++--------- 2 files changed, 57 insertions(+), 30 deletions(-) diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index 10eb67957..63bded04b 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -445,10 +445,10 @@ pub(crate) fn setup_composefs_bls_boot( } }; - kargs_from_composefs_filesystem(fs, &repo, &mut cmdline_refs)?; - let is_upgrade = matches!(setup_type, BootSetupType::Upgrade(..)); + kargs_from_composefs_filesystem(fs, &repo, &mut cmdline_refs, !is_upgrade)?; + let (entry_paths, _tmpdir_guard) = match bootloader { Bootloader::Grub => { let root = Dir::open_ambient_dir(&root_path, ambient_authority()) diff --git a/crates/lib/src/bootc_kargs.rs b/crates/lib/src/bootc_kargs.rs index 88b532697..688b3baf7 100644 --- a/crates/lib/src/bootc_kargs.rs +++ b/crates/lib/src/bootc_kargs.rs @@ -2,6 +2,7 @@ use anyhow::{Context, Result}; use bootc_kernel_cmdline::utf8::{Cmdline, CmdlineOwned}; use camino::Utf8Path; +use cap_std_ext::cap_std::ambient_authority; use cap_std_ext::cap_std::fs::Dir; use cap_std_ext::cap_std::fs_utf8::Dir as DirUtf8; use cap_std_ext::dirext::CapStdExtDirExt; @@ -49,12 +50,52 @@ impl Config { } } +/// Compute the diff between existing and remote kargs +/// Apply the diff to the new kargs +fn compute_apply_kargs_diff( + existing_kargs: &Cmdline, + remote_kargs: &Cmdline, + new_kargs: &mut Cmdline, +) { + // Calculate the diff between the existing and remote kargs + let added_kargs: Vec<_> = remote_kargs + .iter() + .filter(|item| !existing_kargs.iter().any(|existing| *item == existing)) + .collect(); + let removed_kargs: Vec<_> = existing_kargs + .iter() + .filter(|item| !remote_kargs.iter().any(|remote| *item == remote)) + .collect(); + + tracing::debug!("kargs: added={:?} removed={:?}", added_kargs, removed_kargs); + + // Apply the diff to the system kargs + for arg in &removed_kargs { + new_kargs.remove_exact(arg); + } + for arg in &added_kargs { + new_kargs.add(arg); + } +} + /// Looks for files in usr/lib/bootc/kargs.d and parses cmdline agruments pub(crate) fn kargs_from_composefs_filesystem( fs: &ComposefsFilesystem, repo: &ComposefsRepository, - cmdline: &mut Cmdline, + new_kargs: &mut Cmdline, + fresh_install: bool, ) -> Result<()> { + let existing_kargs = match fresh_install { + // If we are freshly installing, we won't have any existing kargs + true => Cmdline::new(), + + false => { + let root = Dir::open_ambient_dir("/", ambient_authority()).context("Opening root")?; + let existing_kargs = get_kargs_in_root(&root, std::env::consts::ARCH)?; + existing_kargs + } + }; + let kargs_d = fs .root .get_directory_opt(OsStr::new(KARGS_PATH)) @@ -64,12 +105,18 @@ pub(crate) fn kargs_from_composefs_filesystem( return Ok(()); }; + let mut remote_kargs = Cmdline::new(); + for (fname, inode) in kargs_d.sorted_entries() { let Inode::Leaf(..) = inode else { continue }; - let fname_str = fname - .to_str() - .ok_or_else(|| anyhow::anyhow!("Failed to get filename as string"))?; + let Some(fname_str) = fname.to_str() else { + tracing::warn!( + "Failed to convert filename to UTF-8, will skip parsing: {:?}", + fname + ); + continue; + }; if !Config::filename_matches(fname_str) { continue; @@ -84,10 +131,12 @@ pub(crate) fn kargs_from_composefs_filesystem( .with_context(|| format!("File {fname_str} is not a valid UTF-8"))?; if let Some(kargs) = parse_kargs_toml(contents, std::env::consts::ARCH)? { - cmdline.extend(&kargs); + remote_kargs.extend(&kargs); }; } + compute_apply_kargs_diff(&existing_kargs, &remote_kargs, new_kargs); + Ok(()) } @@ -221,29 +270,7 @@ pub(crate) fn get_kargs( // Fetch the kernel arguments from the new root let remote_kargs = get_kargs_from_ostree(repo, &fetched_tree, sys_arch)?; - // Calculate the diff between the existing and remote kargs - let added_kargs: Vec<_> = remote_kargs - .iter() - .filter(|item| !existing_kargs.iter().any(|existing| *item == existing)) - .collect(); - let removed_kargs: Vec<_> = existing_kargs - .iter() - .filter(|item| !remote_kargs.iter().any(|remote| *item == remote)) - .collect(); - - tracing::debug!( - "kargs: added={:?} removed={:?}", - &added_kargs, - removed_kargs - ); - - // Apply the diff to the system kargs - for arg in &removed_kargs { - kargs.remove_exact(arg); - } - for arg in &added_kargs { - kargs.add(arg); - } + compute_apply_kargs_diff(&existing_kargs, &remote_kargs, &mut kargs); Ok(kargs) } From dee046de1d664dc6b20ad72210912bd97d19e886 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Fri, 21 Nov 2025 12:56:55 +0530 Subject: [PATCH 4/5] composefs/update: Ensure idempotency on update Handle the following cases we can encounter on `bootc udpate` 1. The verity is the same as that of the currently booted deployment - Nothing to do here in case of update as we're currently booted. But if we're switching then we update the target imageref in the .origin file for the deployment 2. The verity is the same as that of the staged deployment - Nothing to do, as we only get a "staged" deployment if we have /run/composefs/staged-deployment which is the last thing we create while upgrading 3. The verity is the same as that of the rollback deployment or any other deployment we have already deployed - Nothing to do since this is a rollback deployment which means this was unstaged at some point 4. The verity is not found - The update/switch might've been canceled before /run/composefs/staged-deployment was created, or at any other point in time, or it's a new one. Any which way, we can overwrite everything. In this case we remove all the staged bootloader entries, if any, and remove the entire state directory, as it would most probably be in an inconsistent state. Signed-off-by: Pragyan Poudyal --- crates/lib/src/bootc_composefs/state.rs | 49 +++- crates/lib/src/bootc_composefs/update.rs | 281 ++++++++++++++++++----- 2 files changed, 269 insertions(+), 61 deletions(-) diff --git a/crates/lib/src/bootc_composefs/state.rs b/crates/lib/src/bootc_composefs/state.rs index 723d6ed19..196e83284 100644 --- a/crates/lib/src/bootc_composefs/state.rs +++ b/crates/lib/src/bootc_composefs/state.rs @@ -1,4 +1,7 @@ +use std::io::Write; +use std::ops::Deref; use std::os::unix::fs::symlink; +use std::path::Path; use std::{fs::create_dir_all, process::Command}; use anyhow::{Context, Result}; @@ -8,7 +11,7 @@ use bootc_mount::tempmount::TempMount; use bootc_utils::CommandRunExt; use camino::Utf8PathBuf; use cap_std_ext::cap_std::ambient_authority; -use cap_std_ext::cap_std::fs::Dir; +use cap_std_ext::cap_std::fs::{Dir, Permissions, PermissionsExt}; use cap_std_ext::dirext::CapStdExtDirExt; use composefs::fsverity::{FsVerityHashValue, Sha512HashValue}; use fn_error_context::context; @@ -23,6 +26,7 @@ use crate::bootc_composefs::boot::BootType; use crate::bootc_composefs::repo::get_imgref; use crate::bootc_composefs::status::get_sorted_type1_boot_entries; use crate::parsers::bls_config::BLSConfigType; +use crate::store::{BootedComposefs, Storage}; use crate::{ composefs_consts::{ COMPOSEFS_CMDLINE, COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR, @@ -104,6 +108,49 @@ pub(crate) fn copy_etc_to_state( cp_ret } +/// Updates the currently booted image's target imgref +pub(crate) fn update_target_imgref_in_origin( + storage: &Storage, + booted_cfs: &BootedComposefs, + imgref: &ImageReference, +) -> Result<()> { + let path = Path::new(STATE_DIR_RELATIVE).join(booted_cfs.cmdline.digest.deref()); + + let state_dir = storage + .physical_root + .open_dir(path) + .context("Opening state dir")?; + + let origin_filename = format!("{}.origin", booted_cfs.cmdline.digest.deref()); + + let origin_file = state_dir + .read_to_string(&origin_filename) + .context("Reading origin file")?; + + let mut ini = + tini::Ini::from_string(&origin_file).context("Failed to parse file origin file as ini")?; + + // Replace the origin + ini = ini.section("origin").item( + ORIGIN_CONTAINER, + format!("ostree-unverified-image:{imgref}"), + ); + + state_dir + .atomic_replace_with(origin_filename, move |f| -> std::io::Result<_> { + f.write_all(ini.to_string().as_bytes())?; + f.flush()?; + + let perms = Permissions::from_mode(0o644); + f.get_mut().as_file_mut().set_permissions(perms)?; + + Ok(()) + }) + .context("Writing to origin file")?; + + Ok(()) +} + /// Creates and populates /sysroot/state/deploy/image_id #[context("Writing composefs state")] pub(crate) fn write_composefs_state( diff --git a/crates/lib/src/bootc_composefs/update.rs b/crates/lib/src/bootc_composefs/update.rs index eebfd3faa..358312a0d 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -1,6 +1,12 @@ use anyhow::{Context, Result}; use camino::Utf8PathBuf; -use composefs::util::{parse_sha256, Sha256Digest}; +use cap_std_ext::cap_std::fs::Dir; +use composefs::{ + fsverity::{FsVerityHashValue, Sha512HashValue}, + util::{parse_sha256, Sha256Digest}, +}; +use composefs_boot::BootOps; +use composefs_oci::image::create_filesystem; use fn_error_context::context; use ostree_ext::oci_spec::image::{ImageConfiguration, ImageManifest}; @@ -9,11 +15,12 @@ use crate::{ boot::{setup_composefs_bls_boot, setup_composefs_uki_boot, BootSetupType, BootType}, repo::{get_imgref, pull_composefs_repo}, service::start_finalize_stated_svc, - state::write_composefs_state, - status::{get_composefs_status, get_container_manifest_and_config}, + state::{update_target_imgref_in_origin, write_composefs_state}, + status::{get_bootloader, get_composefs_status, get_container_manifest_and_config}, }, cli::UpgradeOpts, - spec::ImageReference, + composefs_consts::{STATE_DIR_RELATIVE, TYPE1_ENT_PATH_STAGED, USER_CFG_STAGED}, + spec::{Bootloader, Host, ImageReference}, store::{BootedComposefs, ComposefsRepository, Storage}, }; @@ -38,14 +45,14 @@ pub fn str_to_sha256digest(id: &str) -> Result { /// # Returns /// /// Returns a tuple containing: -/// * `true` if the image is pulled/available locally, `false` otherwise +/// * `Some` if the image is pulled/available locally, `None` otherwise /// * The container image manifest /// * The container image configuration #[context("Checking if image {} is pulled", imgref.image)] async fn is_image_pulled( repo: &ComposefsRepository, imgref: &ImageReference, -) -> Result<(bool, ImageManifest, ImageConfiguration)> { +) -> Result<(Option, ImageManifest, ImageConfiguration)> { let imgref_repr = get_imgref(&imgref.transport, &imgref.image); let (manifest, config) = get_container_manifest_and_config(&imgref_repr).await?; @@ -55,7 +62,162 @@ async fn is_image_pulled( // check_stream is expensive to run, but probably a good idea let container_pulled = repo.check_stream(&img_sha256).context("Checking stream")?; - Ok((container_pulled.is_some(), manifest, config)) + Ok((container_pulled, manifest, config)) +} + +fn rm_staged_type1_ent(boot_dir: &Dir) -> Result<()> { + if boot_dir.exists(TYPE1_ENT_PATH_STAGED) { + boot_dir + .remove_dir_all(TYPE1_ENT_PATH_STAGED) + .context("Removing staged bootloader entry")?; + } + + Ok(()) +} + +pub(crate) enum UpdateAction { + /// Skip the update. We probably have the update in our deployments + Skip, + /// Proceed with the update + Proceed, + /// Only update the target imgref in the .origin file + UpdateOrigin, +} + +/// Determines what action should be taken for the update +fn validate_update( + storage: &Storage, + booted_cfs: &BootedComposefs, + host: &Host, + img_digest: &str, + config_verity: &Sha512HashValue, +) -> Result { + // Cases + // + // 1. The verity is the same as that of the currently booted deployment + // - Nothing to do here as we're currently booted + // + // 2. The verity is the same as that of the staged deployment + // - Nothing to do, as we only get a "staged" deployment if we have + // /run/composefs/staged-deployment which is the last thing we create while upgrading + // + // 3. The verity is the same as that of the rollback deployment + // - Nothing to do since this is a rollback deployment which means this was unstaged at some + // point + // + // 4. The verity is not found + // - The update/switch might've been canceled before /run/composefs/staged-deployment + // was created, or at any other point in time, or it's a new one. + // Any which way, we can overwrite everything + + let repo = &*booted_cfs.repo; + + let mut fs = create_filesystem(repo, img_digest, Some(config_verity))?; + fs.transform_for_boot(&repo)?; + + let image_id = fs.compute_image_id(); + + // Case1 + // + // "update" image has the same verity as the one currently booted + // This could be someone trying to `bootc switch ` where + // remote_image is the exact same image as the one currently booted, but + // they are wanting to change the target + // + // We could simply update the image origin file here + if image_id.to_hex() == *booted_cfs.cmdline.digest { + // update_target_imgref_in_origin(storage, booted_cfs); + return Ok(UpdateAction::UpdateOrigin); + } + + let all_deployments = host.all_composefs_deployments()?; + + let found_depl = all_deployments + .iter() + .find(|d| d.deployment.verity == image_id.to_hex()); + + // We have this in our deployments somewhere, i.e. Case 2 or 3 + if found_depl.is_some() { + return Ok(UpdateAction::Skip); + } + + let booted = host.require_composefs_booted()?; + let boot_dir = storage.require_boot_dir()?; + + // Remove staged bootloader entries, if any + // GC should take care of the UKI PEs and other binaries + match get_bootloader()? { + Bootloader::Grub => match booted.boot_type { + BootType::Bls => rm_staged_type1_ent(boot_dir)?, + + BootType::Uki => { + let grub = boot_dir.open_dir("grub2").context("Opening grub dir")?; + + if grub.exists(USER_CFG_STAGED) { + grub.remove_file(USER_CFG_STAGED) + .context("Removing staged grub user config")?; + } + } + }, + + Bootloader::Systemd => rm_staged_type1_ent(boot_dir)?, + } + + // Remove state directory + let state_dir = storage + .physical_root + .open_dir(STATE_DIR_RELATIVE) + .context("Opening state dir")?; + + if state_dir.exists(image_id.to_hex()) { + state_dir + .remove_dir_all(image_id.to_hex()) + .context("Removing state")?; + } + + Ok(UpdateAction::Proceed) +} + +async fn do_upgrade(storage: &Storage, host: &Host, imgref: &ImageReference) -> Result<()> { + start_finalize_stated_svc()?; + + let (repo, entries, id, fs) = pull_composefs_repo(&imgref.transport, &imgref.image).await?; + + let Some(entry) = entries.iter().next() else { + anyhow::bail!("No boot entries!"); + }; + + let boot_type = BootType::from(entry); + let mut boot_digest = None; + + match boot_type { + BootType::Bls => { + boot_digest = Some(setup_composefs_bls_boot( + BootSetupType::Upgrade((storage, &fs, &host)), + repo, + &id, + entry, + )?) + } + + BootType::Uki => setup_composefs_uki_boot( + BootSetupType::Upgrade((storage, &fs, &host)), + repo, + &id, + entries, + )?, + }; + + write_composefs_state( + &Utf8PathBuf::from("/sysroot"), + id, + imgref, + true, + boot_type, + boot_digest, + )?; + + Ok(()) } #[context("Upgrading composefs")] @@ -68,7 +230,7 @@ pub(crate) async fn upgrade_composefs( .await .context("Getting composefs deployment status")?; - let mut imgref = host + let mut booted_imgref = host .spec .image .as_ref() @@ -76,17 +238,11 @@ pub(crate) async fn upgrade_composefs( let repo = &*composefs.repo; - let (img_pulled, mut manifest, mut config) = is_image_pulled(&repo, imgref).await?; - let booted_img_digest = manifest.config().digest().digest(); - - // We already have this container config. No update available - if img_pulled { - println!("No changes in: {imgref:#}"); - // TODO(Johan-Liebert1): What if we have the config but we failed the previous update in the middle? - return Ok(()); - } + let (img_pulled, mut manifest, mut config) = is_image_pulled(&repo, booted_imgref).await?; + let booted_img_digest = manifest.config().digest().digest().to_owned(); // Check if we already have this update staged + // Or if we have another staged deployment with a different image let staged_image = host.status.staged.as_ref().and_then(|i| i.image.as_ref()); if let Some(staged_image) = staged_image { @@ -105,16 +261,57 @@ pub(crate) async fn upgrade_composefs( // We have a staged image but it's not the update image. // Maybe it's something we got by `bootc switch` // Switch takes precedence over update, so we change the imgref - imgref = &staged_image.image; + booted_imgref = &staged_image.image; - let (img_pulled, staged_manifest, staged_cfg) = is_image_pulled(&repo, imgref).await?; + let (img_pulled, staged_manifest, staged_cfg) = + is_image_pulled(&repo, booted_imgref).await?; manifest = staged_manifest; config = staged_cfg; - // We already have this container config. No update available - if img_pulled { - println!("No changes in staged image: {imgref:#}"); - return Ok(()); + if let Some(cfg_verity) = img_pulled { + let action = validate_update( + storage, + composefs, + &host, + manifest.config().digest().digest(), + &cfg_verity, + )?; + + match action { + UpdateAction::Skip => { + println!("No changes in staged image: {booted_imgref:#}"); + return Ok(()); + } + + UpdateAction::Proceed => { + return do_upgrade(storage, &host, booted_imgref).await; + } + + UpdateAction::UpdateOrigin => { + // The staged image will never be the current image's verity digest + anyhow::bail!("Staged image verity digest is the same as booted image") + } + } + } + } + + // We already have this container config + if let Some(cfg_verity) = img_pulled { + let action = validate_update(storage, composefs, &host, &booted_img_digest, &cfg_verity)?; + + match action { + UpdateAction::Skip => { + println!("No changes in: {booted_imgref:#}"); + return Ok(()); + } + + UpdateAction::Proceed => { + return do_upgrade(storage, &host, booted_imgref).await; + } + + UpdateAction::UpdateOrigin => { + return update_target_imgref_in_origin(storage, composefs, booted_imgref); + } } } @@ -146,43 +343,7 @@ pub(crate) async fn upgrade_composefs( return Ok(()); } - start_finalize_stated_svc()?; - - let (repo, entries, id, fs) = pull_composefs_repo(&imgref.transport, &imgref.image).await?; - - let Some(entry) = entries.iter().next() else { - anyhow::bail!("No boot entries!"); - }; - - let boot_type = BootType::from(entry); - let mut boot_digest = None; - - match boot_type { - BootType::Bls => { - boot_digest = Some(setup_composefs_bls_boot( - BootSetupType::Upgrade((storage, &fs, &host)), - repo, - &id, - entry, - )?) - } - - BootType::Uki => setup_composefs_uki_boot( - BootSetupType::Upgrade((storage, &fs, &host)), - repo, - &id, - entries, - )?, - }; - - write_composefs_state( - &Utf8PathBuf::from("/sysroot"), - id, - imgref, - true, - boot_type, - boot_digest, - )?; + do_upgrade(storage, &host, booted_imgref).await?; if opts.apply { return crate::reboot::reboot(); From b9ecaac3478f1445c6d3006d21fc5236c12337e1 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Fri, 21 Nov 2025 16:13:17 +0530 Subject: [PATCH 5/5] composefs: Ensure idempotency for switch Similar to how we handle bootc update Signed-off-by: Pragyan Poudyal --- crates/lib/src/bootc_composefs/switch.rs | 66 ++++++++-------- crates/lib/src/bootc_composefs/update.rs | 95 +++++++++++++++++------- 2 files changed, 96 insertions(+), 65 deletions(-) diff --git a/crates/lib/src/bootc_composefs/switch.rs b/crates/lib/src/bootc_composefs/switch.rs index 5cb1e2c1f..8b8a232ec 100644 --- a/crates/lib/src/bootc_composefs/switch.rs +++ b/crates/lib/src/bootc_composefs/switch.rs @@ -1,14 +1,11 @@ use anyhow::{Context, Result}; -use camino::Utf8PathBuf; use fn_error_context::context; use crate::{ bootc_composefs::{ - boot::{setup_composefs_bls_boot, setup_composefs_uki_boot, BootSetupType, BootType}, - repo::pull_composefs_repo, - service::start_finalize_stated_svc, - state::write_composefs_state, + state::update_target_imgref_in_origin, status::get_composefs_status, + update::{do_upgrade, is_image_pulled, validate_update, UpdateAction}, }, cli::{imgref_for_switch, SwitchOpts}, store::{BootedComposefs, Storage}, @@ -42,44 +39,39 @@ pub(crate) async fn switch_composefs( anyhow::bail!("Target image is undefined") }; - start_finalize_stated_svc()?; + let repo = &*booted_cfs.repo; + let (image, manifest, _) = is_image_pulled(repo, &target_imgref).await?; - let (repo, entries, id, fs) = - pull_composefs_repo(&target_imgref.transport, &target_imgref.image).await?; + if let Some(cfg_verity) = image { + let action = validate_update( + storage, + booted_cfs, + &host, + manifest.config().digest().digest(), + &cfg_verity, + true, + )?; - let Some(entry) = entries.iter().next() else { - anyhow::bail!("No boot entries!"); - }; + match action { + UpdateAction::Skip => { + println!("No changes in image: {target_imgref:#}"); + return Ok(()); + } - let boot_type = BootType::from(entry); - let mut boot_digest = None; + UpdateAction::Proceed => { + return do_upgrade(storage, &host, &target_imgref).await; + } - match boot_type { - BootType::Bls => { - boot_digest = Some(setup_composefs_bls_boot( - BootSetupType::Upgrade((storage, &fs, &host)), - repo, - &id, - entry, - )?) + UpdateAction::UpdateOrigin => { + // The staged image will never be the current image's verity digest + println!("Image already in compoesfs repository"); + println!("Updating target image reference"); + return update_target_imgref_in_origin(storage, booted_cfs, &target_imgref); + } } - BootType::Uki => setup_composefs_uki_boot( - BootSetupType::Upgrade((storage, &fs, &host)), - repo, - &id, - entries, - )?, - }; + } - // TODO: Remove this hardcoded path when write_composefs_state accepts a Dir - write_composefs_state( - &Utf8PathBuf::from("/sysroot"), - id, - &target_imgref, - true, - boot_type, - boot_digest, - )?; + do_upgrade(storage, &host, &target_imgref).await?; Ok(()) } diff --git a/crates/lib/src/bootc_composefs/update.rs b/crates/lib/src/bootc_composefs/update.rs index 358312a0d..f9a221e1e 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -15,7 +15,7 @@ use crate::{ boot::{setup_composefs_bls_boot, setup_composefs_uki_boot, BootSetupType, BootType}, repo::{get_imgref, pull_composefs_repo}, service::start_finalize_stated_svc, - state::{update_target_imgref_in_origin, write_composefs_state}, + state::write_composefs_state, status::{get_bootloader, get_composefs_status, get_container_manifest_and_config}, }, cli::UpgradeOpts, @@ -49,7 +49,7 @@ pub fn str_to_sha256digest(id: &str) -> Result { /// * The container image manifest /// * The container image configuration #[context("Checking if image {} is pulled", imgref.image)] -async fn is_image_pulled( +pub(crate) async fn is_image_pulled( repo: &ComposefsRepository, imgref: &ImageReference, ) -> Result<(Option, ImageManifest, ImageConfiguration)> { @@ -75,41 +75,62 @@ fn rm_staged_type1_ent(boot_dir: &Dir) -> Result<()> { Ok(()) } +#[derive(Debug)] pub(crate) enum UpdateAction { /// Skip the update. We probably have the update in our deployments Skip, /// Proceed with the update Proceed, /// Only update the target imgref in the .origin file + /// Will only be returned if the Operation is update and not switch UpdateOrigin, } /// Determines what action should be taken for the update -fn validate_update( +/// +/// Cases: +/// +/// - The verity is the same as that of the currently booted deployment +/// +/// Nothing to do here as we're currently booted +/// +/// - The verity is the same as that of the staged deployment +/// +/// Nothing to do, as we only get a "staged" deployment if we have +/// /run/composefs/staged-deployment which is the last thing we create while upgrading +/// +/// - The verity is the same as that of the rollback deployment +/// +/// Nothing to do since this is a rollback deployment which means this was unstaged at some +/// point +/// +/// - The verity is not found +/// +/// The update/switch might've been canceled before /run/composefs/staged-deployment +/// was created, or at any other point in time, or it's a new one. +/// Any which way, we can overwrite everything +/// +/// # Arguments +/// +/// * `storage` - The global storage object +/// * `booted_cfs` - Reference to the booted composefs deployment +/// * `host` - Object returned by `get_composefs_status` +/// * `img_digest` - The SHA256 sum of the target image +/// * `config_verity` - The verity of the Image config splitstream +/// * `is_switch` - Whether this is an update operation or a switch operation +/// +/// # Returns +/// * UpdateAction::Skip - Skip the update/switch as we have it as a deployment +/// * UpdateAction::UpdateOrigin - Just update the target imgref in the origin file +/// * UpdateAction::Proceed - Proceed with the update +pub(crate) fn validate_update( storage: &Storage, booted_cfs: &BootedComposefs, host: &Host, img_digest: &str, config_verity: &Sha512HashValue, + is_switch: bool, ) -> Result { - // Cases - // - // 1. The verity is the same as that of the currently booted deployment - // - Nothing to do here as we're currently booted - // - // 2. The verity is the same as that of the staged deployment - // - Nothing to do, as we only get a "staged" deployment if we have - // /run/composefs/staged-deployment which is the last thing we create while upgrading - // - // 3. The verity is the same as that of the rollback deployment - // - Nothing to do since this is a rollback deployment which means this was unstaged at some - // point - // - // 4. The verity is not found - // - The update/switch might've been canceled before /run/composefs/staged-deployment - // was created, or at any other point in time, or it's a new one. - // Any which way, we can overwrite everything - let repo = &*booted_cfs.repo; let mut fs = create_filesystem(repo, img_digest, Some(config_verity))?; @@ -126,8 +147,13 @@ fn validate_update( // // We could simply update the image origin file here if image_id.to_hex() == *booted_cfs.cmdline.digest { - // update_target_imgref_in_origin(storage, booted_cfs); - return Ok(UpdateAction::UpdateOrigin); + let ret = if is_switch { + UpdateAction::UpdateOrigin + } else { + UpdateAction::Skip + }; + + return Ok(ret); } let all_deployments = host.all_composefs_deployments()?; @@ -178,7 +204,13 @@ fn validate_update( Ok(UpdateAction::Proceed) } -async fn do_upgrade(storage: &Storage, host: &Host, imgref: &ImageReference) -> Result<()> { +/// Performs the Update or Switch operation +#[context("Performing Upgrade Operation")] +pub(crate) async fn do_upgrade( + storage: &Storage, + host: &Host, + imgref: &ImageReference, +) -> Result<()> { start_finalize_stated_svc()?; let (repo, entries, id, fs) = pull_composefs_repo(&imgref.transport, &imgref.image).await?; @@ -275,6 +307,7 @@ pub(crate) async fn upgrade_composefs( &host, manifest.config().digest().digest(), &cfg_verity, + false, )?; match action { @@ -288,8 +321,7 @@ pub(crate) async fn upgrade_composefs( } UpdateAction::UpdateOrigin => { - // The staged image will never be the current image's verity digest - anyhow::bail!("Staged image verity digest is the same as booted image") + anyhow::bail!("Updating origin not supported for update operation") } } } @@ -297,7 +329,14 @@ pub(crate) async fn upgrade_composefs( // We already have this container config if let Some(cfg_verity) = img_pulled { - let action = validate_update(storage, composefs, &host, &booted_img_digest, &cfg_verity)?; + let action = validate_update( + storage, + composefs, + &host, + &booted_img_digest, + &cfg_verity, + false, + )?; match action { UpdateAction::Skip => { @@ -310,7 +349,7 @@ pub(crate) async fn upgrade_composefs( } UpdateAction::UpdateOrigin => { - return update_target_imgref_in_origin(storage, composefs, booted_imgref); + anyhow::bail!("Updating origin not supported for update operation") } } }