diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index 30ecc5ccc..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::{ @@ -1142,7 +1147,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 +1222,13 @@ pub(crate) fn setup_composefs_boot( false, boot_type, boot_digest, - )?; + &get_container_manifest_and_config(&get_imgref( + &state.source.imageref.transport.to_string(), + &state.source.imageref.name, + )) + .await?, + ) + .await?; Ok(()) } diff --git a/crates/lib/src/bootc_composefs/state.rs b/crates/lib/src/bootc_composefs/state.rs index 18ef34e0a..402157e5a 100644 --- a/crates/lib/src/bootc_composefs/state.rs +++ b/crates/lib/src/bootc_composefs/state.rs @@ -24,7 +24,7 @@ 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_sorted_type1_boot_entries, ImgConfigManifest}; use crate::parsers::bls_config::BLSConfigType; use crate::store::{BootedComposefs, Storage}; use crate::{ @@ -151,15 +151,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` - Container manifest and config used to create this deployment +/// +/// # 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, + target_imgref: &ImageReference, staged: bool, boot_type: BootType, boot_digest: Option, + container_details: &ImgConfigManifest, ) -> Result<()> { let state_path = root_path .join(STATE_DIR_RELATIVE) @@ -183,7 +209,7 @@ pub(crate) fn write_composefs_state( image: image_name, transport, .. - } = &imgref; + } = &target_imgref; let imgref = get_imgref(&transport, &image_name); @@ -206,6 +232,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(&container_details)?, + ) + .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..c4967824e 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -3,9 +3,10 @@ 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, + 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::{ @@ -20,12 +21,12 @@ 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::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")] @@ -173,49 +181,84 @@ pub(crate) fn get_bootloader() -> Result { } } +/// Reads the .imginfo file for the provided deployment +#[context("Reading 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 mut img_conf = storage + .physical_root + .open_optional(&path) + .context("Failed to open file")?; + + 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) +} + #[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 img_conf = get_imginfo(storage, &verity, &img_ref).await?; - Err(e) => { - tracing::debug!("Failed to open image {img_ref}, because {e:?}"); - ("".into(), None, "".into(), None) - } - }; + 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 +356,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..f6942dc8f 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -8,7 +8,7 @@ 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 ostree_ext::container::ManifestDiff; use crate::{ bootc_composefs::{ @@ -16,7 +16,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, get_imginfo, + ImgConfigManifest, + }, }, cli::UpgradeOpts, composefs_consts::{STATE_DIR_RELATIVE, TYPE1_ENT_PATH_STAGED, USER_CFG_STAGED}, @@ -52,17 +55,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 +214,7 @@ pub(crate) async fn do_upgrade( storage: &Storage, host: &Host, imgref: &ImageReference, + img_manifest_config: &ImgConfigManifest, ) -> Result<()> { start_finalize_stated_svc()?; @@ -255,7 +259,9 @@ pub(crate) async fn do_upgrade( true, boot_type, boot_digest, - )?; + img_manifest_config, + ) + .await?; Ok(()) } @@ -278,8 +284,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 +309,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 +329,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 +357,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 => { @@ -363,34 +367,14 @@ 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() { - 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 >= manifest.layers().len() { - anyhow::bail!("Length mismatch between rootfs diff layers and manifest layers"); - } - - let layer = &manifest.layers()[idx]; - - println!( - "Added layer: {}\tSize: {}", - layer.digest(), - layer.size().to_string() - ); - } - } - + 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(()); } - 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?; }