diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index 1a295d07..801a019b 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -1277,7 +1277,7 @@ pub(crate) async fn setup_composefs_boot( &root_setup.physical_root_path, &id, &crate::spec::ImageReference::from(state.target_imgref.clone()), - false, + None, boot_type, boot_digest, &get_container_manifest_and_config(&get_imgref( diff --git a/crates/lib/src/bootc_composefs/finalize.rs b/crates/lib/src/bootc_composefs/finalize.rs index 03b62e2e..0f8ffab0 100644 --- a/crates/lib/src/bootc_composefs/finalize.rs +++ b/crates/lib/src/bootc_composefs/finalize.rs @@ -57,6 +57,11 @@ pub(crate) async fn composefs_backend_finalize( return Ok(()); }; + if staged_depl.download_only { + tracing::debug!("Staged deployment is marked download only. Won't finalize"); + return Ok(()); + } + let staged_composefs = staged_depl.composefs.as_ref().ok_or(anyhow::anyhow!( "Staged deployment is not a composefs deployment" ))?; diff --git a/crates/lib/src/bootc_composefs/state.rs b/crates/lib/src/bootc_composefs/state.rs index 0dc20f8b..517281be 100644 --- a/crates/lib/src/bootc_composefs/state.rs +++ b/crates/lib/src/bootc_composefs/state.rs @@ -9,6 +9,7 @@ use bootc_kernel_cmdline::utf8::Cmdline; use bootc_mount::tempmount::TempMount; use bootc_utils::CommandRunExt; use camino::Utf8PathBuf; +use canon_json::CanonJsonSerialize; use cap_std_ext::cap_std::ambient_authority; use cap_std_ext::cap_std::fs::{Dir, Permissions, PermissionsExt}; use cap_std_ext::dirext::CapStdExtDirExt; @@ -23,7 +24,9 @@ use rustix::{ use crate::bootc_composefs::boot::BootType; use crate::bootc_composefs::repo::get_imgref; -use crate::bootc_composefs::status::{ImgConfigManifest, get_sorted_type1_boot_entries}; +use crate::bootc_composefs::status::{ + ImgConfigManifest, StagedDeployment, get_sorted_type1_boot_entries, +}; use crate::parsers::bls_config::BLSConfigType; use crate::store::{BootedComposefs, Storage}; use crate::{ @@ -227,7 +230,7 @@ pub(crate) async fn write_composefs_state( root_path: &Utf8PathBuf, deployment_id: &Sha512HashValue, target_imgref: &ImageReference, - staged: bool, + staged: Option, boot_type: BootType, boot_digest: String, container_details: &ImgConfigManifest, @@ -248,7 +251,12 @@ pub(crate) async fn write_composefs_state( ) .context("Failed to create symlink for /var")?; - initialize_state(&root_path, &deployment_id.to_hex(), &state_path, !staged)?; + initialize_state( + &root_path, + &deployment_id.to_hex(), + &state_path, + staged.is_none(), + )?; let ImageReference { image: image_name, @@ -291,7 +299,7 @@ pub(crate) async fn write_composefs_state( ) .context("Failed to write to .origin file")?; - if staged { + if let Some(staged) = staged { std::fs::create_dir_all(COMPOSEFS_TRANSIENT_STATE_DIR) .with_context(|| format!("Creating {COMPOSEFS_TRANSIENT_STATE_DIR}"))?; @@ -302,7 +310,9 @@ pub(crate) async fn write_composefs_state( staged_depl_dir .atomic_write( COMPOSEFS_STAGED_DEPLOYMENT_FNAME, - deployment_id.to_hex().as_bytes(), + staged + .to_canon_json_vec() + .context("Failed to serialize staged deployment JSON")?, ) .with_context(|| format!("Writing to {COMPOSEFS_STAGED_DEPLOYMENT_FNAME}"))?; } diff --git a/crates/lib/src/bootc_composefs/status.rs b/crates/lib/src/bootc_composefs/status.rs index 386e2470..c5da0d20 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -79,6 +79,12 @@ impl std::fmt::Display for ComposefsCmdline { } } +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct StagedDeployment { + pub(crate) depl_id: String, + pub(crate) finalization_locked: bool, +} + /// Detect if we have composefs= in /proc/cmdline pub(crate) fn composefs_booted() -> Result> { static CACHED_DIGEST_VALUE: OnceLock> = OnceLock::new(); @@ -554,7 +560,7 @@ pub(crate) async fn composefs_deployment_status_from( let mut host = Host::new(host_spec); - let staged_deployment_id = match std::fs::File::open(format!( + let staged_deployment = match std::fs::File::open(format!( "{COMPOSEFS_TRANSIENT_STATE_DIR}/{COMPOSEFS_STAGED_DEPLOYMENT_FNAME}" )) { Ok(mut f) => { @@ -590,7 +596,7 @@ pub(crate) async fn composefs_deployment_status_from( let ini = tini::Ini::from_string(&config) .with_context(|| format!("Failed to parse file {depl_file_name}.origin as ini"))?; - let boot_entry = + let mut boot_entry = boot_entry_from_composefs_deployment(storage, ini, depl_file_name.to_string()).await?; // SAFETY: boot_entry.composefs will always be present @@ -614,8 +620,11 @@ pub(crate) async fn composefs_deployment_status_from( continue; } - if let Some(staged_deployment_id) = &staged_deployment_id { - if depl_file_name == staged_deployment_id.trim() { + if let Some(staged_deployment) = &staged_deployment { + let staged_depl = serde_json::from_str::(&staged_deployment)?; + + if depl_file_name == staged_depl.depl_id { + boot_entry.download_only = staged_depl.finalization_locked; host.status.staged = Some(boot_entry); continue; } diff --git a/crates/lib/src/bootc_composefs/switch.rs b/crates/lib/src/bootc_composefs/switch.rs index 4f12b479..944e166c 100644 --- a/crates/lib/src/bootc_composefs/switch.rs +++ b/crates/lib/src/bootc_composefs/switch.rs @@ -45,6 +45,7 @@ pub(crate) async fn switch_composefs( let do_upgrade_opts = DoUpgradeOpts { soft_reboot: opts.soft_reboot, apply: opts.apply, + download_only: false, }; if let Some(cfg_verity) = image { diff --git a/crates/lib/src/bootc_composefs/update.rs b/crates/lib/src/bootc_composefs/update.rs index 667105cf..8b74f4c0 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -1,10 +1,14 @@ +use std::io::Write; + use anyhow::{Context, Result}; use camino::Utf8PathBuf; -use cap_std_ext::cap_std::fs::Dir; +use canon_json::CanonJsonSerialize; +use cap_std_ext::{cap_std::fs::Dir, dirext::CapStdExtDirExt}; use composefs::fsverity::{FsVerityHashValue, Sha512HashValue}; use composefs_boot::BootOps; use composefs_oci::image::create_filesystem; use fn_error_context::context; +use ocidir::cap_std::ambient_authority; use ostree_ext::container::ManifestDiff; use crate::{ @@ -15,12 +19,15 @@ use crate::{ soft_reboot::prepare_soft_reboot_composefs, state::write_composefs_state, status::{ - ImgConfigManifest, get_bootloader, get_composefs_status, + ImgConfigManifest, StagedDeployment, get_bootloader, get_composefs_status, get_container_manifest_and_config, get_imginfo, }, }, cli::{SoftRebootMode, UpgradeOpts}, - composefs_consts::{STATE_DIR_RELATIVE, TYPE1_ENT_PATH_STAGED, USER_CFG_STAGED}, + composefs_consts::{ + COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR, STATE_DIR_RELATIVE, + TYPE1_ENT_PATH_STAGED, USER_CFG_STAGED, + }, spec::{Bootloader, Host, ImageReference}, store::{BootedComposefs, ComposefsRepository, Storage}, }; @@ -206,6 +213,31 @@ pub(crate) fn validate_update( pub(crate) struct DoUpgradeOpts { pub(crate) apply: bool, pub(crate) soft_reboot: Option, + pub(crate) download_only: bool, +} + +async fn apply_upgrade( + storage: &Storage, + booted_cfs: &BootedComposefs, + depl_id: &String, + opts: &DoUpgradeOpts, +) -> Result<()> { + if let Some(soft_reboot_mode) = opts.soft_reboot { + return prepare_soft_reboot_composefs( + storage, + booted_cfs, + Some(depl_id), + soft_reboot_mode, + opts.apply, + ) + .await; + }; + + if opts.apply { + return crate::reboot::reboot(); + } + + Ok(()) } /// Performs the Update or Switch operation @@ -255,29 +287,17 @@ pub(crate) async fn do_upgrade( &Utf8PathBuf::from("/sysroot"), &id, imgref, - true, + Some(StagedDeployment { + depl_id: id.to_hex(), + finalization_locked: opts.download_only, + }), boot_type, boot_digest, img_manifest_config, ) .await?; - if let Some(soft_reboot_mode) = opts.soft_reboot { - return prepare_soft_reboot_composefs( - storage, - booted_cfs, - Some(&id.to_hex()), - soft_reboot_mode, - opts.apply, - ) - .await; - }; - - if opts.apply { - return crate::reboot::reboot(); - } - - Ok(()) + apply_upgrade(storage, booted_cfs, &id.to_hex(), opts).await } #[context("Upgrading composefs")] @@ -286,18 +306,60 @@ pub(crate) async fn upgrade_composefs( storage: &Storage, composefs: &BootedComposefs, ) -> Result<()> { - // Download-only mode is not yet supported for composefs backend - if opts.download_only { - anyhow::bail!("--download-only is not yet supported for composefs backend"); - } - if opts.from_downloaded { - anyhow::bail!("--from-downloaded is not yet supported for composefs backend"); - } - let host = get_composefs_status(storage, composefs) .await .context("Getting composefs deployment status")?; + let do_upgrade_opts = DoUpgradeOpts { + soft_reboot: opts.soft_reboot, + apply: opts.apply, + download_only: opts.download_only, + }; + + if opts.from_downloaded { + let staged = host + .status + .staged + .as_ref() + .ok_or_else(|| anyhow::anyhow!("No staged deployment found"))?; + + // Staged deployment exists, but it will be finalized + if !staged.download_only { + println!("Staged deployment is present and not in download only mode."); + println!("Use `bootc update --apply` to apply the update."); + return Ok(()); + } + + start_finalize_stated_svc()?; + + // Make the staged deployment not download_only + let new_staged = StagedDeployment { + depl_id: staged.require_composefs()?.verity.clone(), + finalization_locked: false, + }; + + let staged_depl_dir = + Dir::open_ambient_dir(COMPOSEFS_TRANSIENT_STATE_DIR, ambient_authority()) + .context("Opening transient state directory")?; + + staged_depl_dir + .atomic_replace_with( + COMPOSEFS_STAGED_DEPLOYMENT_FNAME, + |f| -> std::io::Result<()> { + f.write_all(new_staged.to_canon_json_string()?.as_bytes()) + }, + ) + .context("Writing staged file")?; + + return apply_upgrade( + storage, + composefs, + &staged.require_composefs()?.verity, + &do_upgrade_opts, + ) + .await; + } + let mut booted_imgref = host .spec .image @@ -313,11 +375,6 @@ pub(crate) async fn upgrade_composefs( // 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()); - let do_upgrade_opts = DoUpgradeOpts { - soft_reboot: opts.soft_reboot, - apply: opts.apply, - }; - if let Some(staged_image) = staged_image { // We have a staged image and it has the same digest as the currently booted image's latest // digest