From 66222e9efa55542b38b2cd7709d370da3d02bf66 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 17 Sep 2025 14:55:36 +0530 Subject: [PATCH 1/5] composese-backend: Implement install to filesystem Signed-off-by: Pragyan Poudyal --- crates/lib/src/install.rs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index defa9d19c..0733038b1 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -372,6 +372,14 @@ pub(crate) struct InstallToFilesystemOpts { #[clap(flatten)] pub(crate) config_opts: InstallConfigOpts, + + #[clap(long)] + #[cfg(feature = "composefs-backend")] + pub(crate) composefs_native: bool, + + #[cfg(feature = "composefs-backend")] + #[clap(flatten)] + pub(crate) compoesfs_opts: InstallComposefsOpts, } #[derive(Debug, Clone, clap::Parser, PartialEq, Eq)] @@ -1861,7 +1869,17 @@ pub(crate) async fn install_to_filesystem( // IMPORTANT: and hence anything that is done before MUST BE IDEMPOTENT. // IMPORTANT: In practice, we should only be gathering information before this point, // IMPORTANT: and not performing any mutations at all. - let state = prepare_install(opts.config_opts, opts.source_opts, opts.target_opts, None).await?; + let state = prepare_install( + opts.config_opts, + opts.source_opts, + opts.target_opts, + #[cfg(feature = "composefs-backend")] + opts.composefs_native.then_some(opts.compoesfs_opts), + #[cfg(not(feature = "composefs-backend"))] + None, + ) + .await?; + // And the last bit of state here is the fsopts, which we also destructure now. let mut fsopts = opts.filesystem_opts; @@ -2129,6 +2147,14 @@ pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) -> source_opts: opts.source_opts, target_opts: opts.target_opts, config_opts: opts.config_opts, + #[cfg(feature = "composefs-backend")] + composefs_native: false, + #[cfg(feature = "composefs-backend")] + compoesfs_opts: InstallComposefsOpts { + insecure: false, + bootloader: Bootloader::Grub, + uki_addon: None, + }, }; install_to_filesystem(opts, true, cleanup).await From 0c1cb6bcd914959b221e427ba8ab131c65f1833f Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 8 Oct 2025 11:51:48 +0530 Subject: [PATCH 2/5] composefs-backend: Rename 'composefs-native' to 'composefs-backend' We were using composefs-native and composefs-backend interchangeably. Replace all instances of `composefs-native` with `composefs-backend` Move all composefs-backend options to a single struct so that we can test for boolean instead of testing for Some/None for composefs-backend options Signed-off-by: Pragyan Poudyal --- crates/lib/src/bootc_composefs/boot.rs | 29 +++------- crates/lib/src/bootc_composefs/finalize.rs | 2 +- crates/lib/src/cli.rs | 4 +- crates/lib/src/composefs_consts.rs | 4 +- crates/lib/src/install.rs | 56 ++++++++----------- tmt/tests/examples/bootc-uki/install-grub.sh | 2 +- .../bootc-uki/install-systemd-boot.sh | 2 +- 7 files changed, 39 insertions(+), 60 deletions(-) diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index 4dae7d5cb..e59944f16 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -353,14 +353,11 @@ pub(crate) fn setup_composefs_bls_boot( // root_setup.kargs has [root=UUID=, "rw"] let mut cmdline_options = String::from(root_setup.kargs.join(" ")); - match &state.composefs_options { - Some(opt) if opt.insecure => { - cmdline_options.push_str(&format!(" {COMPOSEFS_CMDLINE}=?{id_hex}")); - } - None | Some(..) => { - cmdline_options.push_str(&format!(" {COMPOSEFS_CMDLINE}={id_hex}")); - } - }; + if state.composefs_options.insecure { + cmdline_options.push_str(&format!(" {COMPOSEFS_CMDLINE}=?{id_hex}")); + } else { + cmdline_options.push_str(&format!(" {COMPOSEFS_CMDLINE}={id_hex}")); + } // Locate ESP partition device let esp_part = root_setup @@ -375,11 +372,7 @@ pub(crate) fn setup_composefs_bls_boot( esp_part.node.clone(), cmdline_options, fs, - state - .composefs_options - .as_ref() - .map(|opts| opts.bootloader.clone()) - .unwrap_or(Bootloader::default()), + state.composefs_options.bootloader.clone(), ) } @@ -828,10 +821,6 @@ pub(crate) fn setup_composefs_uki_boot( } } - let Some(cfs_opts) = &state.composefs_options else { - anyhow::bail!("ComposeFS options not found"); - }; - let esp_part = root_setup .device_info .partitions @@ -842,9 +831,9 @@ pub(crate) fn setup_composefs_uki_boot( ( root_setup.physical_root_path.clone(), esp_part.node.clone(), - cfs_opts.bootloader.clone(), - cfs_opts.insecure, - cfs_opts.uki_addon.as_ref(), + state.composefs_options.bootloader.clone(), + state.composefs_options.insecure, + state.composefs_options.uki_addon.as_ref(), ) } diff --git a/crates/lib/src/bootc_composefs/finalize.rs b/crates/lib/src/bootc_composefs/finalize.rs index 22971009d..e09e21350 100644 --- a/crates/lib/src/bootc_composefs/finalize.rs +++ b/crates/lib/src/bootc_composefs/finalize.rs @@ -39,7 +39,7 @@ pub(crate) async fn get_etc_diff() -> Result<()> { Ok(()) } -pub(crate) async fn composefs_native_finalize() -> Result<()> { +pub(crate) async fn composefs_backend_finalize() -> Result<()> { let host = composefs_deployment_status().await?; let booted_composefs = host.require_composefs_booted()?; diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index 3412a6b03..cf54b9e3c 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -31,7 +31,7 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "composefs-backend")] use crate::bootc_composefs::{ - finalize::{composefs_native_finalize, get_etc_diff}, + finalize::{composefs_backend_finalize, get_etc_diff}, rollback::composefs_rollback, state::composefs_usr_overlay, status::composefs_booted, @@ -1544,7 +1544,7 @@ async fn run_from_opt(opt: Opt) -> Result<()> { }, #[cfg(feature = "composefs-backend")] - Opt::ComposefsFinalizeStaged => composefs_native_finalize().await, + Opt::ComposefsFinalizeStaged => composefs_backend_finalize().await, #[cfg(feature = "composefs-backend")] Opt::ConfigDiff => get_etc_diff().await, diff --git a/crates/lib/src/composefs_consts.rs b/crates/lib/src/composefs_consts.rs index 828504a86..453576cad 100644 --- a/crates/lib/src/composefs_consts.rs +++ b/crates/lib/src/composefs_consts.rs @@ -8,9 +8,9 @@ pub(crate) const COMPOSEFS_TRANSIENT_STATE_DIR: &str = "/run/composefs"; /// File created in /run/composefs to record a staged-deployment pub(crate) const COMPOSEFS_STAGED_DEPLOYMENT_FNAME: &str = "staged-deployment"; -/// Absolute path to composefs-native state directory +/// Absolute path to composefs-backend state directory pub(crate) const STATE_DIR_ABS: &str = "/sysroot/state/deploy"; -/// Relative path to composefs-native state directory. Relative to /sysroot +/// Relative path to composefs-backend state directory. Relative to /sysroot pub(crate) const STATE_DIR_RELATIVE: &str = "state/deploy"; /// Relative path to the shared 'var' directory. Relative to /sysroot pub(crate) const SHARED_VAR_PATH: &str = "state/os/default/var"; diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index 0733038b1..2fa3081d6 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -249,10 +249,21 @@ pub(crate) struct InstallConfigOpts { #[derive(Debug, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)] pub(crate) struct InstallComposefsOpts { + /// If true, composefs backend is used, else ostree backend is used + #[clap(long, default_value_t)] + #[serde(default)] + pub(crate) composefs_backend: bool, + + /// Make fs-verity validation optional in case the filesystem doesn't support it #[clap(long, default_value_t)] #[serde(default)] pub(crate) insecure: bool, + /// The bootloader to use. + /// Currently supported: + /// + /// - systemd-boot + /// - grub #[clap(long, default_value_t)] #[serde(default)] pub(crate) bootloader: Bootloader, @@ -288,11 +299,6 @@ pub(crate) struct InstallToDiskOpts { #[serde(default)] pub(crate) via_loopback: bool, - #[clap(long)] - #[serde(default)] - #[cfg(feature = "composefs-backend")] - pub(crate) composefs_native: bool, - #[clap(flatten)] #[serde(flatten)] #[cfg(feature = "composefs-backend")] @@ -373,10 +379,6 @@ pub(crate) struct InstallToFilesystemOpts { #[clap(flatten)] pub(crate) config_opts: InstallConfigOpts, - #[clap(long)] - #[cfg(feature = "composefs-backend")] - pub(crate) composefs_native: bool, - #[cfg(feature = "composefs-backend")] #[clap(flatten)] pub(crate) compoesfs_opts: InstallComposefsOpts, @@ -446,9 +448,9 @@ pub(crate) struct State { pub(crate) container_root: Dir, pub(crate) tempdir: TempDir, - // If Some, then --composefs_native is passed + // If Some, then --composefs-backend is passed #[cfg(feature = "composefs-backend")] - pub(crate) composefs_options: Option, + pub(crate) composefs_options: InstallComposefsOpts, } impl State { @@ -578,10 +580,10 @@ impl FromStr for MountSpec { #[cfg(all(feature = "install-to-disk", feature = "composefs-backend"))] impl InstallToDiskOpts { pub(crate) fn validate(&self) -> Result<()> { - if !self.composefs_native { - // Reject using --insecure without --composefs + if !self.composefs_opts.composefs_backend { + // Reject using --insecure without --composefs-backend if self.composefs_opts.insecure != false { - anyhow::bail!("--insecure must not be provided without --composefs"); + anyhow::bail!("--insecure must not be provided without --composefs-backend"); } } @@ -1234,7 +1236,7 @@ async fn prepare_install( config_opts: InstallConfigOpts, source_opts: InstallSourceOpts, target_opts: InstallTargetOpts, - _composefs_opts: Option, + #[cfg(feature = "composefs-backend")] composefs_options: InstallComposefsOpts, ) -> Result> { tracing::trace!("Preparing install"); let rootfs = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority()) @@ -1380,7 +1382,7 @@ async fn prepare_install( tempdir, host_is_container, #[cfg(feature = "composefs-backend")] - composefs_options: _composefs_opts, + composefs_options, }); Ok(state) @@ -1545,7 +1547,7 @@ async fn install_to_filesystem_impl( } #[cfg(feature = "composefs-backend")] - if state.composefs_options.is_some() { + if state.composefs_options.composefs_backend { // Load a fd for the mounted target physical root let (id, verity) = initialize_composefs_repository(state, rootfs).await?; @@ -1622,21 +1624,12 @@ pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> { anyhow::bail!("Not a block device: {}", block_opts.device); } - #[cfg(feature = "composefs-backend")] - let composefs_arg = if opts.composefs_native { - Some(opts.composefs_opts) - } else { - None - }; - - #[cfg(not(feature = "composefs-backend"))] - let composefs_arg = None; - let state = prepare_install( opts.config_opts, opts.source_opts, opts.target_opts, - composefs_arg, + #[cfg(feature = "composefs-backend")] + opts.composefs_opts, ) .await?; @@ -1874,9 +1867,7 @@ pub(crate) async fn install_to_filesystem( opts.source_opts, opts.target_opts, #[cfg(feature = "composefs-backend")] - opts.composefs_native.then_some(opts.compoesfs_opts), - #[cfg(not(feature = "composefs-backend"))] - None, + opts.compoesfs_opts, ) .await?; @@ -2148,9 +2139,8 @@ pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) -> target_opts: opts.target_opts, config_opts: opts.config_opts, #[cfg(feature = "composefs-backend")] - composefs_native: false, - #[cfg(feature = "composefs-backend")] compoesfs_opts: InstallComposefsOpts { + composefs_backend: true, insecure: false, bootloader: Bootloader::Grub, uki_addon: None, diff --git a/tmt/tests/examples/bootc-uki/install-grub.sh b/tmt/tests/examples/bootc-uki/install-grub.sh index 885826046..6a9b0bd60 100755 --- a/tmt/tests/examples/bootc-uki/install-grub.sh +++ b/tmt/tests/examples/bootc-uki/install-grub.sh @@ -19,7 +19,7 @@ podman run \ --security-opt label=type:unconfined_t \ "${IMAGE}" \ bootc install to-disk \ - --composefs-native \ + --composefs-backend \ --boot=uki \ --source-imgref="containers-storage:${IMAGE}" \ --target-imgref="${IMAGE}" \ diff --git a/tmt/tests/examples/bootc-uki/install-systemd-boot.sh b/tmt/tests/examples/bootc-uki/install-systemd-boot.sh index 08e92107b..9eca959a8 100755 --- a/tmt/tests/examples/bootc-uki/install-systemd-boot.sh +++ b/tmt/tests/examples/bootc-uki/install-systemd-boot.sh @@ -24,7 +24,7 @@ podman run \ --security-opt label=type:unconfined_t \ "${IMAGE}" \ bootc install to-disk \ - --composefs-native \ + --composefs-backend \ --boot=uki \ --source-imgref="containers-storage:${IMAGE}" \ --target-imgref="${IMAGE}" \ From faa68a7cd6635dd258e14c5edb3ad51734e9cbab Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Thu, 25 Sep 2025 16:58:08 +0530 Subject: [PATCH 3/5] composefs-backend/state: Store target imgref Instead of storing the source imgref in the .origin file, we store the target imgref Signed-off-by: Pragyan Poudyal --- crates/lib/src/bootc_composefs/boot.rs | 7 +------ crates/lib/src/bootc_composefs/state.rs | 4 +++- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index e59944f16..e77c2bd20 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -38,7 +38,6 @@ use crate::bootc_composefs::status::get_sorted_uki_boot_entries; use crate::composefs_consts::{TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED}; use crate::parsers::bls_config::{BLSConfig, BLSConfigType}; use crate::parsers::grub_menuconfig::MenuEntry; -use crate::spec::ImageReference; use crate::task::Task; use crate::{ composefs_consts::{ @@ -981,11 +980,7 @@ pub(crate) fn setup_composefs_boot( write_composefs_state( &root_setup.physical_root_path, id, - &ImageReference { - image: state.source.imageref.name.clone(), - transport: state.source.imageref.transport.to_string(), - signature: None, - }, + &crate::spec::ImageReference::from(state.target_imgref.clone()), false, boot_type, boot_digest, diff --git a/crates/lib/src/bootc_composefs/state.rs b/crates/lib/src/bootc_composefs/state.rs index b163d3717..5244f7c62 100644 --- a/crates/lib/src/bootc_composefs/state.rs +++ b/crates/lib/src/bootc_composefs/state.rs @@ -113,7 +113,9 @@ pub(crate) fn write_composefs_state( boot_type: BootType, boot_digest: Option, ) -> Result<()> { - let state_path = root_path.join(format!("{STATE_DIR_RELATIVE}/{}", deployment_id.to_hex())); + let state_path = root_path + .join(STATE_DIR_RELATIVE) + .join(deployment_id.to_hex()); create_dir_all(state_path.join("etc"))?; From ebe252c2dbad57f251e80244beff5ce14cffd7b7 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 8 Oct 2025 11:36:16 +0530 Subject: [PATCH 4/5] composefs-backend: Start finalize-staged service on update/switch Signed-off-by: Pragyan Poudyal --- crates/lib/src/bootc_composefs/mod.rs | 1 + crates/lib/src/bootc_composefs/repo.rs | 2 +- crates/lib/src/bootc_composefs/service.rs | 22 ++++++++++++++++++++++ crates/lib/src/bootc_composefs/switch.rs | 3 +++ crates/lib/src/bootc_composefs/update.rs | 5 +++-- crates/lib/src/composefs_consts.rs | 2 ++ 6 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 crates/lib/src/bootc_composefs/service.rs diff --git a/crates/lib/src/bootc_composefs/mod.rs b/crates/lib/src/bootc_composefs/mod.rs index c19dbfb77..ee8a742ed 100644 --- a/crates/lib/src/bootc_composefs/mod.rs +++ b/crates/lib/src/bootc_composefs/mod.rs @@ -2,6 +2,7 @@ pub(crate) mod boot; pub(crate) mod finalize; pub(crate) mod repo; pub(crate) mod rollback; +pub(crate) mod service; pub(crate) mod state; pub(crate) mod status; pub(crate) mod switch; diff --git a/crates/lib/src/bootc_composefs/repo.rs b/crates/lib/src/bootc_composefs/repo.rs index 51323c716..aaae00c18 100644 --- a/crates/lib/src/bootc_composefs/repo.rs +++ b/crates/lib/src/bootc_composefs/repo.rs @@ -95,7 +95,7 @@ pub(crate) async fn pull_composefs_repo( .await .context("Pulling composefs repo")?; - tracing::info!("id: {}, verity: {}", hex::encode(id), verity.to_hex()); + tracing::info!("ID: {}, Verity: {}", hex::encode(id), verity.to_hex()); let repo = open_composefs_repo(&rootfs_dir)?; let mut fs = create_composefs_filesystem(&repo, &hex::encode(id), None) diff --git a/crates/lib/src/bootc_composefs/service.rs b/crates/lib/src/bootc_composefs/service.rs new file mode 100644 index 000000000..66163a7bb --- /dev/null +++ b/crates/lib/src/bootc_composefs/service.rs @@ -0,0 +1,22 @@ +use anyhow::{Context, Result}; +use fn_error_context::context; +use std::process::Command; + +use crate::composefs_consts::COMPOSEFS_FINALIZE_STAGED_SERVICE; + +/// Starts the finaize staged service which will "unstage" the deployment +/// This is called before an upgrade or switch operation, as these create a staged +/// deployment +#[context("Starting finalize staged service")] +pub(crate) fn start_finalize_stated_svc() -> Result<()> { + let cmd_status = Command::new("systemctl") + .args(["start", "--quiet", COMPOSEFS_FINALIZE_STAGED_SERVICE]) + .status() + .context("Starting finalize service")?; + + if !cmd_status.success() { + anyhow::bail!("systemctl exited with status {cmd_status}") + } + + Ok(()) +} diff --git a/crates/lib/src/bootc_composefs/switch.rs b/crates/lib/src/bootc_composefs/switch.rs index bebb95399..485fc59db 100644 --- a/crates/lib/src/bootc_composefs/switch.rs +++ b/crates/lib/src/bootc_composefs/switch.rs @@ -6,6 +6,7 @@ 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, status::composefs_deployment_status, }, @@ -36,6 +37,8 @@ pub(crate) async fn switch_composefs(opts: SwitchOpts) -> Result<()> { anyhow::bail!("Target image is undefined") }; + start_finalize_stated_svc()?; + let (repo, entries, id, fs) = pull_composefs_repo(&target_imgref.transport, &target_imgref.image).await?; diff --git a/crates/lib/src/bootc_composefs/update.rs b/crates/lib/src/bootc_composefs/update.rs index 823a50bed..018d8f3ed 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -6,6 +6,7 @@ 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, status::composefs_deployment_status, }, @@ -14,12 +15,12 @@ use crate::{ #[context("Upgrading composefs")] pub(crate) async fn upgrade_composefs(_opts: UpgradeOpts) -> Result<()> { - // TODO: IMPORTANT Have all the checks here that `bootc upgrade` has for an ostree booted system - let host = composefs_deployment_status() .await .context("Getting composefs deployment status")?; + start_finalize_stated_svc()?; + // TODO: IMPORTANT We need to check if any deployment is staged and get the image from that let imgref = host .spec diff --git a/crates/lib/src/composefs_consts.rs b/crates/lib/src/composefs_consts.rs index 453576cad..6d6dfb944 100644 --- a/crates/lib/src/composefs_consts.rs +++ b/crates/lib/src/composefs_consts.rs @@ -36,3 +36,5 @@ pub(crate) const USER_CFG_STAGED: &str = "user.cfg.staged"; /// This is relative to the boot/efi directory pub(crate) const TYPE1_ENT_PATH: &str = "loader/entries"; pub(crate) const TYPE1_ENT_PATH_STAGED: &str = "loader/entries.staged"; + +pub(crate) const COMPOSEFS_FINALIZE_STAGED_SERVICE: &str = "composefs-finalize-staged.service"; From b0b66d31954e0210d1bd10e2afa9368aa5ea74f5 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 8 Oct 2025 13:54:25 +0530 Subject: [PATCH 5/5] composefs-backend: Add composefs opts to install-to-existing-root Signed-off-by: Pragyan Poudyal --- crates/lib/src/install.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index 2fa3081d6..673f34d56 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -413,6 +413,10 @@ pub(crate) struct InstallToExistingRootOpts { /// via e.g. `-v /:/target`. #[clap(default_value = ALONGSIDE_ROOT_MOUNT)] pub(crate) root_path: Utf8PathBuf, + + #[cfg(feature = "composefs-backend")] + #[clap(flatten)] + pub(crate) composefs_opts: InstallComposefsOpts, } /// Global state captured from the container. @@ -2139,12 +2143,7 @@ pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) -> target_opts: opts.target_opts, config_opts: opts.config_opts, #[cfg(feature = "composefs-backend")] - compoesfs_opts: InstallComposefsOpts { - composefs_backend: true, - insecure: false, - bootloader: Bootloader::Grub, - uki_addon: None, - }, + compoesfs_opts: opts.composefs_opts, }; install_to_filesystem(opts, true, cleanup).await