From ec40dee66d70c6690718757061148a9f9c2f8def Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Fri, 5 Dec 2025 15:52:22 +0530 Subject: [PATCH 1/4] composefs: Store image manifest and config Until now, when doing a `bootc status` for a compoesfs booted system, we were reaching out a container registry to fetch image manifest and config, which is pretty suboptimal as the command took upwards of 1.5s to execute, sometimes. Instead, now we store the manifest + config as a JSON structure inside an `.imginfo` file alongside the `.origin` file Signed-off-by: Pragyan Poudyal --- crates/lib/src/bootc_composefs/boot.rs | 6 ++- crates/lib/src/bootc_composefs/state.rs | 48 ++++++++++++++++-- crates/lib/src/bootc_composefs/status.rs | 62 +++++++++++++----------- crates/lib/src/bootc_composefs/switch.rs | 8 +-- crates/lib/src/bootc_composefs/update.rs | 43 ++++++++-------- crates/lib/src/install.rs | 2 +- 6 files changed, 111 insertions(+), 58 deletions(-) diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index 30ecc5ccc..fbafd9c5e 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -1142,7 +1142,7 @@ pub(crate) fn setup_composefs_uki_boot( } #[context("Setting up composefs boot")] -pub(crate) fn setup_composefs_boot( +pub(crate) async fn setup_composefs_boot( root_setup: &RootSetup, state: &State, image_id: &str, @@ -1217,7 +1217,9 @@ pub(crate) fn setup_composefs_boot( false, boot_type, boot_digest, - )?; + None, + ) + .await?; Ok(()) } diff --git a/crates/lib/src/bootc_composefs/state.rs b/crates/lib/src/bootc_composefs/state.rs index 18ef34e0a..b27c54b2b 100644 --- a/crates/lib/src/bootc_composefs/state.rs +++ b/crates/lib/src/bootc_composefs/state.rs @@ -24,7 +24,9 @@ use rustix::{ 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::bootc_composefs::status::{ + get_container_manifest_and_config, get_sorted_type1_boot_entries, ImgConfigManifest, +}; use crate::parsers::bls_config::BLSConfigType; use crate::store::{BootedComposefs, Storage}; use crate::{ @@ -151,15 +153,41 @@ pub(crate) fn update_target_imgref_in_origin( Ok(()) } -/// Creates and populates /sysroot/state/deploy/image_id +/// Creates and populates the composefs state directory for a deployment. +/// +/// This function sets up the state directory structure and configuration files +/// needed for a composefs deployment. It creates the deployment state directory, +/// copies configuration, sets up the shared `/var` directory, and writes metadata +/// files including the origin configuration and image information. +/// +/// # Arguments +/// +/// * `root_path` - The root filesystem path (typically `/sysroot`) +/// * `deployment_id` - Unique SHA512 hash identifier for this deployment +/// * `imgref` - Container image reference for the deployment +/// * `staged` - Whether this is a staged deployment (writes to transient state dir) +/// * `boot_type` - Boot loader type (`Bls` or `Uki`) +/// * `boot_digest` - Optional boot digest for verification +/// * `container_details` - Optional container manifest and config. Fetched if not provided +/// +/// # State Directory Structure +/// +/// Creates the following structure under `/sysroot/state/deploy/{deployment_id}/`: +/// * `etc/` - Copy of system configuration files +/// * `var` - Symlink to shared `/var` directory +/// * `{deployment_id}.origin` - OSTree-style origin configuration +/// * `{deployment_id}.imginfo` - Container image manifest and config as JSON +/// +/// For staged deployments, also writes to `/run/composefs/staged-deployment`. #[context("Writing composefs state")] -pub(crate) fn write_composefs_state( +pub(crate) async fn write_composefs_state( root_path: &Utf8PathBuf, deployment_id: Sha512HashValue, imgref: &ImageReference, staged: bool, boot_type: BootType, boot_digest: Option, + container_details: Option<&ImgConfigManifest>, ) -> Result<()> { let state_path = root_path .join(STATE_DIR_RELATIVE) @@ -187,6 +215,11 @@ pub(crate) fn write_composefs_state( let imgref = get_imgref(&transport, &image_name); + let img_config = match container_details { + Some(val) => val, + None => &get_container_manifest_and_config(&imgref).await?, + }; + let mut config = tini::Ini::new().section("origin").item( ORIGIN_CONTAINER, // TODO (Johan-Liebert1): The image won't always be unverified @@ -206,6 +239,15 @@ pub(crate) fn write_composefs_state( let state_dir = Dir::open_ambient_dir(&state_path, ambient_authority()).context("Opening state dir")?; + // NOTE: This is only supposed to be temporary until we decide on where to store + // the container manifest/config + state_dir + .atomic_write( + format!("{}.imginfo", deployment_id.to_hex()), + serde_json::to_vec(&img_config)?, + ) + .context("Failed to write to .imginfo file")?; + state_dir .atomic_write( format!("{}.origin", deployment_id.to_hex()), diff --git a/crates/lib/src/bootc_composefs/status.rs b/crates/lib/src/bootc_composefs/status.rs index 0194d0c33..d28d724ee 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -3,6 +3,7 @@ use std::{io::Read, sync::OnceLock}; use anyhow::{Context, Result}; use bootc_kernel_cmdline::utf8::Cmdline; use fn_error_context::context; +use serde::{Deserialize, Serialize}; use crate::{ bootc_composefs::boot::BootType, @@ -22,10 +23,10 @@ use std::str::FromStr; use bootc_utils::try_deserialize_timestamp; use cap_std_ext::cap_std::fs::Dir; use ostree_container::OstreeImageReference; -use ostree_ext::container::deploy::ORIGIN_CONTAINER; use ostree_ext::container::{self as ostree_container}; use ostree_ext::containers_image_proxy; use ostree_ext::oci_spec; +use ostree_ext::{container::deploy::ORIGIN_CONTAINER, oci_spec::image::ImageConfiguration}; use ostree_ext::oci_spec::image::ImageManifest; use tokio::io::AsyncReadExt; @@ -36,6 +37,13 @@ use crate::composefs_consts::{ }; use crate::spec::Bootloader; +/// Used for storing the container image info alongside of .origin file +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct ImgConfigManifest { + pub(crate) config: ImageConfiguration, + pub(crate) manifest: ImageManifest, +} + /// A parsed composefs command line #[derive(Clone)] pub(crate) struct ComposefsCmdline { @@ -134,7 +142,7 @@ pub(crate) fn get_sorted_type1_boot_entries( #[context("Getting container info")] pub(crate) async fn get_container_manifest_and_config( imgref: &String, -) -> Result<(ImageManifest, oci_spec::image::ImageConfiguration)> { +) -> Result { let config = containers_image_proxy::ImageProxyConfig::default(); let proxy = containers_image_proxy::ImageProxy::new_with_config(config).await?; @@ -150,7 +158,7 @@ pub(crate) async fn get_container_manifest_and_config( let config: oci_spec::image::ImageConfiguration = serde_json::from_slice(&buf)?; - Ok((manifest, config)) + Ok(ImgConfigManifest { manifest, config }) } #[context("Getting bootloader")] @@ -175,47 +183,45 @@ pub(crate) fn get_bootloader() -> Result { #[context("Getting composefs deployment metadata")] async fn boot_entry_from_composefs_deployment( + storage: &Storage, origin: tini::Ini, verity: String, ) -> Result { let image = match origin.get::("origin", ORIGIN_CONTAINER) { Some(img_name_from_config) => { let ostree_img_ref = OstreeImageReference::from_str(&img_name_from_config)?; - let imgref = ostree_img_ref.imgref.to_string(); let img_ref = ImageReference::from(ostree_img_ref); - // The image might've been removed, so don't error if we can't get the image manifest - let (image_digest, version, architecture, created_at) = - match get_container_manifest_and_config(&imgref).await { - Ok((manifest, config)) => { - let digest = manifest.config().digest().to_string(); - let arch = config.architecture().to_string(); - let created = config.created().clone(); - let version = manifest - .annotations() - .as_ref() - .and_then(|a| a.get(oci_spec::image::ANNOTATION_VERSION).cloned()); - - (digest, version, arch, created) - } + let path = std::path::PathBuf::from(STATE_DIR_RELATIVE) + .join(&verity) + .join(format!("{verity}.imginfo")); - Err(e) => { - tracing::debug!("Failed to open image {img_ref}, because {e:?}"); - ("".into(), None, "".into(), None) - } - }; + let img_conf = storage + .physical_root + .read_to_string(&path) + .context("Failed to open imginfo file")?; + + let img_conf: ImgConfigManifest = + serde_json::from_str(&img_conf).context("Failed to parse imginfo file as JSON")?; + let image_digest = img_conf.manifest.config().digest().to_string(); + let architecture = img_conf.config.architecture().to_string(); + let version = img_conf + .manifest + .annotations() + .as_ref() + .and_then(|a| a.get(oci_spec::image::ANNOTATION_VERSION).cloned()); + + let created_at = img_conf.config.created().clone(); let timestamp = created_at.and_then(|x| try_deserialize_timestamp(&x)); - let image_status = ImageStatus { + Some(ImageStatus { image: img_ref, version, timestamp, image_digest, architecture, - }; - - Some(image_status) + }) } // Wasn't booted using a container image. Do nothing @@ -313,7 +319,7 @@ pub(crate) async fn composefs_deployment_status_from( .with_context(|| format!("Failed to parse file {depl_file_name}.origin as ini"))?; let boot_entry = - boot_entry_from_composefs_deployment(ini, depl_file_name.to_string()).await?; + boot_entry_from_composefs_deployment(storage, ini, depl_file_name.to_string()).await?; // SAFETY: boot_entry.composefs will always be present let boot_type_from_origin = boot_entry.composefs.as_ref().unwrap().boot_type; diff --git a/crates/lib/src/bootc_composefs/switch.rs b/crates/lib/src/bootc_composefs/switch.rs index 4f0190f54..2ce890e16 100644 --- a/crates/lib/src/bootc_composefs/switch.rs +++ b/crates/lib/src/bootc_composefs/switch.rs @@ -40,14 +40,14 @@ pub(crate) async fn switch_composefs( }; let repo = &*booted_cfs.repo; - let (image, manifest, _) = is_image_pulled(repo, &target_imgref).await?; + let (image, img_config) = is_image_pulled(repo, &target_imgref).await?; if let Some(cfg_verity) = image { let action = validate_update( storage, booted_cfs, &host, - manifest.config().digest().digest(), + img_config.manifest.config().digest().digest(), &cfg_verity, true, )?; @@ -59,7 +59,7 @@ pub(crate) async fn switch_composefs( } UpdateAction::Proceed => { - return do_upgrade(storage, &host, &target_imgref).await; + return do_upgrade(storage, &host, &target_imgref, &img_config).await; } UpdateAction::UpdateOrigin => { @@ -71,7 +71,7 @@ pub(crate) async fn switch_composefs( } } - do_upgrade(storage, &host, &target_imgref).await?; + do_upgrade(storage, &host, &target_imgref, &img_config).await?; Ok(()) } diff --git a/crates/lib/src/bootc_composefs/update.rs b/crates/lib/src/bootc_composefs/update.rs index 37578fad4..0d124ea76 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -8,7 +8,6 @@ use composefs::{ use composefs_boot::BootOps; use composefs_oci::image::create_filesystem; use fn_error_context::context; -use ostree_ext::oci_spec::image::{ImageConfiguration, ImageManifest}; use crate::{ bootc_composefs::{ @@ -16,7 +15,10 @@ use crate::{ repo::{get_imgref, pull_composefs_repo}, service::start_finalize_stated_svc, state::write_composefs_state, - status::{get_bootloader, get_composefs_status, get_container_manifest_and_config}, + status::{ + get_bootloader, get_composefs_status, get_container_manifest_and_config, + ImgConfigManifest, + }, }, cli::UpgradeOpts, composefs_consts::{STATE_DIR_RELATIVE, TYPE1_ENT_PATH_STAGED, USER_CFG_STAGED}, @@ -52,17 +54,17 @@ pub fn str_to_sha256digest(id: &str) -> Result { pub(crate) async fn is_image_pulled( repo: &ComposefsRepository, imgref: &ImageReference, -) -> Result<(Option, ImageManifest, ImageConfiguration)> { +) -> Result<(Option, ImgConfigManifest)> { let imgref_repr = get_imgref(&imgref.transport, &imgref.image); - let (manifest, config) = get_container_manifest_and_config(&imgref_repr).await?; + let img_config_manifest = get_container_manifest_and_config(&imgref_repr).await?; - let img_digest = manifest.config().digest().digest(); + let img_digest = img_config_manifest.manifest.config().digest().digest(); let img_sha256 = str_to_sha256digest(&img_digest)?; // 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, manifest, config)) + Ok((container_pulled, img_config_manifest)) } fn rm_staged_type1_ent(boot_dir: &Dir) -> Result<()> { @@ -211,6 +213,7 @@ pub(crate) async fn do_upgrade( storage: &Storage, host: &Host, imgref: &ImageReference, + img_manifest_config: &ImgConfigManifest, ) -> Result<()> { start_finalize_stated_svc()?; @@ -255,7 +258,9 @@ pub(crate) async fn do_upgrade( true, boot_type, boot_digest, - )?; + Some(img_manifest_config), + ) + .await?; Ok(()) } @@ -278,8 +283,8 @@ pub(crate) async fn upgrade_composefs( let repo = &*composefs.repo; - let (img_pulled, mut manifest, mut config) = is_image_pulled(&repo, booted_imgref).await?; - let booted_img_digest = manifest.config().digest().digest().to_owned(); + let (img_pulled, mut img_config) = is_image_pulled(&repo, booted_imgref).await?; + let booted_img_digest = img_config.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 @@ -303,17 +308,15 @@ pub(crate) async fn upgrade_composefs( // Switch takes precedence over update, so we change the imgref booted_imgref = &staged_image.image; - let (img_pulled, staged_manifest, staged_cfg) = - is_image_pulled(&repo, booted_imgref).await?; - manifest = staged_manifest; - config = staged_cfg; + let (img_pulled, staged_img_config) = is_image_pulled(&repo, booted_imgref).await?; + img_config = staged_img_config; if let Some(cfg_verity) = img_pulled { let action = validate_update( storage, composefs, &host, - manifest.config().digest().digest(), + img_config.manifest.config().digest().digest(), &cfg_verity, false, )?; @@ -325,7 +328,7 @@ pub(crate) async fn upgrade_composefs( } UpdateAction::Proceed => { - return do_upgrade(storage, &host, booted_imgref).await; + return do_upgrade(storage, &host, booted_imgref, &img_config).await; } UpdateAction::UpdateOrigin => { @@ -353,7 +356,7 @@ pub(crate) async fn upgrade_composefs( } UpdateAction::Proceed => { - return do_upgrade(storage, &host, booted_imgref).await; + return do_upgrade(storage, &host, booted_imgref, &img_config).await; } UpdateAction::UpdateOrigin => { @@ -365,7 +368,7 @@ pub(crate) async fn upgrade_composefs( if opts.check { // TODO(Johan-Liebert1): If we have the previous, i.e. the current manifest with us then we can replace the // following with [`ostree_container::ManifestDiff::new`] which will be much cleaner - for (idx, diff_id) in config.rootfs().diff_ids().iter().enumerate() { + for (idx, diff_id) in img_config.config.rootfs().diff_ids().iter().enumerate() { let diff_id = str_to_sha256digest(diff_id)?; // we could use `check_stream` here but that will most probably take forever as it @@ -373,11 +376,11 @@ pub(crate) async fn upgrade_composefs( let have_layer = repo.has_stream(&diff_id)?; if have_layer.is_none() { - if idx >= manifest.layers().len() { + if idx >= img_config.manifest.layers().len() { anyhow::bail!("Length mismatch between rootfs diff layers and manifest layers"); } - let layer = &manifest.layers()[idx]; + let layer = &img_config.manifest.layers()[idx]; println!( "Added layer: {}\tSize: {}", @@ -390,7 +393,7 @@ pub(crate) async fn upgrade_composefs( return Ok(()); } - do_upgrade(storage, &host, booted_imgref).await?; + do_upgrade(storage, &host, booted_imgref, &img_config).await?; if opts.apply { return crate::reboot::reboot(); diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index b12607bb5..ba35ef47f 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -1689,7 +1689,7 @@ async fn install_to_filesystem_impl( let (id, verity) = initialize_composefs_repository(state, rootfs).await?; tracing::info!("id: {}, verity: {}", hex::encode(id), verity.to_hex()); - setup_composefs_boot(rootfs, state, &hex::encode(id))?; + setup_composefs_boot(rootfs, state, &hex::encode(id)).await?; } else { ostree_install(state, rootfs, cleanup).await?; } From ab4ff7458a51eb820645b27796d85eac9ec03cf2 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Fri, 5 Dec 2025 16:12:59 +0530 Subject: [PATCH 2/4] composefs: Use `ManifestDiff` for getting manifest diff Now that we store the current deployment's manifest locally, we can replace the ugly stuff with a simple call to `ManifestDiff::new` Signed-off-by: Pragyan Poudyal --- crates/lib/src/bootc_composefs/status.rs | 30 +++++++++++++++--------- crates/lib/src/bootc_composefs/update.rs | 30 ++++-------------------- 2 files changed, 24 insertions(+), 36 deletions(-) diff --git a/crates/lib/src/bootc_composefs/status.rs b/crates/lib/src/bootc_composefs/status.rs index d28d724ee..10ed165d0 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -181,6 +181,24 @@ pub(crate) fn get_bootloader() -> Result { } } +/// Reads the .imginfo file for the provided deployment +#[context("Reading imginfo")] +pub(crate) fn get_imginfo(storage: &Storage, deployment_id: &str) -> Result { + let path = std::path::PathBuf::from(STATE_DIR_RELATIVE) + .join(deployment_id) + .join(format!("{deployment_id}.imginfo")); + + let img_conf = storage + .physical_root + .read_to_string(&path) + .context("Failed to open file")?; + + let img_conf = serde_json::from_str::(&img_conf) + .context("Failed to parse file as JSON")?; + + Ok(img_conf) +} + #[context("Getting composefs deployment metadata")] async fn boot_entry_from_composefs_deployment( storage: &Storage, @@ -192,17 +210,7 @@ async fn boot_entry_from_composefs_deployment( let ostree_img_ref = OstreeImageReference::from_str(&img_name_from_config)?; let img_ref = ImageReference::from(ostree_img_ref); - let path = std::path::PathBuf::from(STATE_DIR_RELATIVE) - .join(&verity) - .join(format!("{verity}.imginfo")); - - let img_conf = storage - .physical_root - .read_to_string(&path) - .context("Failed to open imginfo file")?; - - let img_conf: ImgConfigManifest = - serde_json::from_str(&img_conf).context("Failed to parse imginfo file as JSON")?; + let img_conf = get_imginfo(storage, &verity)?; let image_digest = img_conf.manifest.config().digest().to_string(); let architecture = img_conf.config.architecture().to_string(); diff --git a/crates/lib/src/bootc_composefs/update.rs b/crates/lib/src/bootc_composefs/update.rs index 0d124ea76..b4486c88f 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -8,6 +8,7 @@ use composefs::{ use composefs_boot::BootOps; use composefs_oci::image::create_filesystem; use fn_error_context::context; +use ostree_ext::container::ManifestDiff; use crate::{ bootc_composefs::{ @@ -16,7 +17,7 @@ use crate::{ service::start_finalize_stated_svc, state::write_composefs_state, status::{ - get_bootloader, get_composefs_status, get_container_manifest_and_config, + get_bootloader, get_composefs_status, get_container_manifest_and_config, get_imginfo, ImgConfigManifest, }, }, @@ -366,30 +367,9 @@ pub(crate) async fn upgrade_composefs( } if opts.check { - // TODO(Johan-Liebert1): If we have the previous, i.e. the current manifest with us then we can replace the - // following with [`ostree_container::ManifestDiff::new`] which will be much cleaner - for (idx, diff_id) in img_config.config.rootfs().diff_ids().iter().enumerate() { - let diff_id = str_to_sha256digest(diff_id)?; - - // we could use `check_stream` here but that will most probably take forever as it - // usually takes ~3s to verify one single layer - let have_layer = repo.has_stream(&diff_id)?; - - if have_layer.is_none() { - if idx >= img_config.manifest.layers().len() { - anyhow::bail!("Length mismatch between rootfs diff layers and manifest layers"); - } - - let layer = &img_config.manifest.layers()[idx]; - - println!( - "Added layer: {}\tSize: {}", - layer.digest(), - layer.size().to_string() - ); - } - } - + let current_manifest = get_imginfo(storage, &*composefs.cmdline.digest)?; + let diff = ManifestDiff::new(¤t_manifest.manifest, &img_config.manifest); + diff.print(); return Ok(()); } From 235c2721900bb7d72f1edf84384fafb1a9695daa Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Mon, 8 Dec 2025 16:51:13 +0530 Subject: [PATCH 3/4] composefs/state: Require container details when writing state Make `container_details` argument mandatory while writing composefs deployment state. It's better to fetch the container details from the source imgref, rather than the target imgref, as the target might not even exist at the time of deployment. Fixes CI failures as we were fetching from local registry (according to the target imgref), which doesn't exist Signed-off-by: Pragyan Poudyal --- crates/lib/src/bootc_composefs/boot.rs | 15 ++++++++++++--- crates/lib/src/bootc_composefs/state.rs | 21 ++++++++------------- crates/lib/src/bootc_composefs/update.rs | 2 +- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index fbafd9c5e..94d9261f8 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -96,11 +96,13 @@ use rustix::{mount::MountFlags, path::Arg}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::bootc_kargs::compute_new_kargs; -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::task::Task; +use crate::{ + bootc_composefs::repo::get_imgref, + composefs_consts::{TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED}, +}; use crate::{ bootc_composefs::repo::open_composefs_repo, store::{ComposefsFilesystem, Storage}, @@ -109,6 +111,9 @@ use crate::{ bootc_composefs::state::{get_booted_bls, write_composefs_state}, bootloader::esp_in, }; +use crate::{ + bootc_composefs::status::get_container_manifest_and_config, bootc_kargs::compute_new_kargs, +}; use crate::{bootc_composefs::status::get_sorted_grub_uki_boot_entries, install::PostFetchState}; use crate::{ composefs_consts::{ @@ -1217,7 +1222,11 @@ pub(crate) async fn setup_composefs_boot( false, boot_type, boot_digest, - None, + &get_container_manifest_and_config(&get_imgref( + &state.source.imageref.transport.to_string(), + &state.source.imageref.name, + )) + .await?, ) .await?; diff --git a/crates/lib/src/bootc_composefs/state.rs b/crates/lib/src/bootc_composefs/state.rs index b27c54b2b..e52ac4a5c 100644 --- a/crates/lib/src/bootc_composefs/state.rs +++ b/crates/lib/src/bootc_composefs/state.rs @@ -24,9 +24,7 @@ use rustix::{ use crate::bootc_composefs::boot::BootType; use crate::bootc_composefs::repo::get_imgref; -use crate::bootc_composefs::status::{ - get_container_manifest_and_config, get_sorted_type1_boot_entries, ImgConfigManifest, -}; +use crate::bootc_composefs::status::{get_sorted_type1_boot_entries, ImgConfigManifest}; use crate::parsers::bls_config::BLSConfigType; use crate::store::{BootedComposefs, Storage}; use crate::{ @@ -168,7 +166,7 @@ pub(crate) fn update_target_imgref_in_origin( /// * `staged` - Whether this is a staged deployment (writes to transient state dir) /// * `boot_type` - Boot loader type (`Bls` or `Uki`) /// * `boot_digest` - Optional boot digest for verification -/// * `container_details` - Optional container manifest and config. Fetched if not provided +/// * `container_details` - Container manifest and config used to create this deployment /// /// # State Directory Structure /// @@ -183,11 +181,11 @@ pub(crate) fn update_target_imgref_in_origin( pub(crate) async fn write_composefs_state( root_path: &Utf8PathBuf, deployment_id: Sha512HashValue, - imgref: &ImageReference, + target_imgref: &ImageReference, staged: bool, boot_type: BootType, boot_digest: Option, - container_details: Option<&ImgConfigManifest>, + container_details: &ImgConfigManifest, ) -> Result<()> { let state_path = root_path .join(STATE_DIR_RELATIVE) @@ -211,14 +209,11 @@ pub(crate) async fn write_composefs_state( image: image_name, transport, .. - } = &imgref; + } = &target_imgref; - let imgref = get_imgref(&transport, &image_name); + println!("imgref: {target_imgref:#?}"); - let img_config = match container_details { - Some(val) => val, - None => &get_container_manifest_and_config(&imgref).await?, - }; + let imgref = get_imgref(&transport, &image_name); let mut config = tini::Ini::new().section("origin").item( ORIGIN_CONTAINER, @@ -244,7 +239,7 @@ pub(crate) async fn write_composefs_state( state_dir .atomic_write( format!("{}.imginfo", deployment_id.to_hex()), - serde_json::to_vec(&img_config)?, + serde_json::to_vec(&container_details)?, ) .context("Failed to write to .imginfo file")?; diff --git a/crates/lib/src/bootc_composefs/update.rs b/crates/lib/src/bootc_composefs/update.rs index b4486c88f..04d94e27d 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -259,7 +259,7 @@ pub(crate) async fn do_upgrade( true, boot_type, boot_digest, - Some(img_manifest_config), + img_manifest_config, ) .await?; From f6f46d2d2465fa47ae8407cc92f01451eb2a1f9a Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Thu, 11 Dec 2025 14:24:01 +0530 Subject: [PATCH 4/4] composefs: Backwards compatibility for fetching image info For older systems that do not have the `.imginfo` file, we revert back to our older logic of fetching the image info from registry We then store this in an `.imginfo` file so we don't go and make a network call on every subsequent command Signed-off-by: Pragyan Poudyal --- crates/lib/src/bootc_composefs/state.rs | 2 - crates/lib/src/bootc_composefs/status.rs | 49 +++++++++++++++++++----- crates/lib/src/bootc_composefs/update.rs | 3 +- 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/crates/lib/src/bootc_composefs/state.rs b/crates/lib/src/bootc_composefs/state.rs index e52ac4a5c..402157e5a 100644 --- a/crates/lib/src/bootc_composefs/state.rs +++ b/crates/lib/src/bootc_composefs/state.rs @@ -211,8 +211,6 @@ pub(crate) async fn write_composefs_state( .. } = &target_imgref; - println!("imgref: {target_imgref:#?}"); - let imgref = get_imgref(&transport, &image_name); let mut config = tini::Ini::new().section("origin").item( diff --git a/crates/lib/src/bootc_composefs/status.rs b/crates/lib/src/bootc_composefs/status.rs index 10ed165d0..c4967824e 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -6,7 +6,7 @@ use fn_error_context::context; use serde::{Deserialize, Serialize}; use crate::{ - bootc_composefs::boot::BootType, + bootc_composefs::{boot::BootType, repo::get_imgref}, composefs_consts::{COMPOSEFS_CMDLINE, ORIGIN_KEY_BOOT_DIGEST, TYPE1_ENT_PATH, USER_CFG}, install::EFI_LOADER_INFO, parsers::{ @@ -21,7 +21,7 @@ use crate::{ use std::str::FromStr; use bootc_utils::try_deserialize_timestamp; -use cap_std_ext::cap_std::fs::Dir; +use cap_std_ext::{cap_std::fs::Dir, dirext::CapStdExtDirExt}; use ostree_container::OstreeImageReference; use ostree_ext::container::{self as ostree_container}; use ostree_ext::containers_image_proxy; @@ -183,17 +183,46 @@ pub(crate) fn get_bootloader() -> Result { /// Reads the .imginfo file for the provided deployment #[context("Reading imginfo")] -pub(crate) fn get_imginfo(storage: &Storage, deployment_id: &str) -> Result { - let path = std::path::PathBuf::from(STATE_DIR_RELATIVE) - .join(deployment_id) - .join(format!("{deployment_id}.imginfo")); +pub(crate) async fn get_imginfo( + storage: &Storage, + deployment_id: &str, + imgref: &ImageReference, +) -> Result { + let imginfo_fname = format!("{deployment_id}.imginfo"); + + let depl_state_path = std::path::PathBuf::from(STATE_DIR_RELATIVE).join(deployment_id); + let path = depl_state_path.join(imginfo_fname); - let img_conf = storage + let mut img_conf = storage .physical_root - .read_to_string(&path) + .open_optional(&path) .context("Failed to open file")?; - let img_conf = serde_json::from_str::(&img_conf) + let Some(img_conf) = &mut img_conf else { + let container_details = + get_container_manifest_and_config(&get_imgref(&imgref.transport, &imgref.image)) + .await?; + + let state_dir = storage.physical_root.open_dir(depl_state_path)?; + + state_dir + .atomic_write( + format!("{}.imginfo", deployment_id), + serde_json::to_vec(&container_details)?, + ) + .context("Failed to write to .imginfo file")?; + + let state_dir = state_dir.reopen_as_ownedfd()?; + + rustix::fs::fsync(state_dir).context("fsync")?; + + return Ok(container_details); + }; + + let mut buffer = String::new(); + img_conf.read_to_string(&mut buffer)?; + + let img_conf = serde_json::from_str::(&buffer) .context("Failed to parse file as JSON")?; Ok(img_conf) @@ -210,7 +239,7 @@ async fn boot_entry_from_composefs_deployment( let ostree_img_ref = OstreeImageReference::from_str(&img_name_from_config)?; let img_ref = ImageReference::from(ostree_img_ref); - let img_conf = get_imginfo(storage, &verity)?; + let img_conf = get_imginfo(storage, &verity, &img_ref).await?; let image_digest = img_conf.manifest.config().digest().to_string(); let architecture = img_conf.config.architecture().to_string(); diff --git a/crates/lib/src/bootc_composefs/update.rs b/crates/lib/src/bootc_composefs/update.rs index 04d94e27d..f6942dc8f 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -367,7 +367,8 @@ pub(crate) async fn upgrade_composefs( } if opts.check { - let current_manifest = get_imginfo(storage, &*composefs.cmdline.digest)?; + let current_manifest = + get_imginfo(storage, &*composefs.cmdline.digest, booted_imgref).await?; let diff = ManifestDiff::new(¤t_manifest.manifest, &img_config.manifest); diff.print(); return Ok(());