diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index 590c0c4a7..881c01b49 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -15,8 +15,11 @@ use cap_std_ext::{ use clap::ValueEnum; use composefs::fs::read_file; use composefs::tree::RegularFile; -use composefs_boot::bootloader::{PEType, EFI_ADDON_DIR_EXT, EFI_ADDON_FILE_EXT, EFI_EXT}; use composefs_boot::BootOps; +use composefs_boot::{ + bootloader::{PEType, EFI_ADDON_DIR_EXT, EFI_ADDON_FILE_EXT, EFI_EXT}, + uki::UkiError, +}; use fn_error_context::context; use ostree_ext::composefs::fsverity::{FsVerityHashValue, Sha512HashValue}; use ostree_ext::composefs_boot::bootloader::UsrLibModulesVmlinuz; @@ -365,7 +368,6 @@ struct BLSEntryPath<'a> { #[context("Setting up BLS boot")] pub(crate) fn setup_composefs_bls_boot( setup_type: BootSetupType, - // TODO: Make this generic repo: crate::store::ComposefsRepository, id: &Sha512HashValue, entry: &ComposefsBootEntry, @@ -563,6 +565,11 @@ pub(crate) fn setup_composefs_bls_boot( Ok(boot_digest) } +struct UKILabels { + boot_label: String, + version: Option, +} + /// Writes a PortableExecutable to ESP along with any PE specific or Global addons #[context("Writing {file_path} to ESP")] fn write_pe_to_esp( @@ -574,10 +581,10 @@ fn write_pe_to_esp( is_insecure_from_opts: bool, mounted_efi: impl AsRef, bootloader: &Bootloader, -) -> Result> { +) -> Result> { let efi_bin = read_file(file, &repo).context("Reading .efi binary")?; - let mut boot_label = None; + let mut boot_label: Option = None; // UKI Extension might not even have a cmdline // TODO: UKI Addon might also have a composefs= cmdline? @@ -607,7 +614,15 @@ fn write_pe_to_esp( ); } - boot_label = Some(uki::get_boot_label(&efi_bin).context("Getting UKI boot label")?); + let osrel = uki::get_text_section(&efi_bin, ".osrel") + .ok_or(UkiError::PortableExecutableError)??; + + let parsed_osrel = OsReleaseInfo::parse(osrel); + + boot_label = Some(UKILabels { + boot_label: uki::get_boot_label(&efi_bin).context("Getting UKI boot label")?, + version: parsed_osrel.get_version(), + }); } // Write the UKI to ESP @@ -693,8 +708,6 @@ fn write_grub_uki_menuentry( .context("opening boot/grub2")?; // Iterate over all available deployments, and generate a menuentry for each - // - // TODO: We might find a staged deployment here if is_upgrade { let mut str_buf = String::new(); let boot_dir = @@ -758,20 +771,19 @@ fn write_grub_uki_menuentry( fn write_systemd_uki_config( esp_dir: &Dir, setup_type: &BootSetupType, - boot_label: String, + boot_label: UKILabels, id: &Sha512HashValue, ) -> Result<()> { let default_sort_key = "0"; let mut bls_conf = BLSConfig::default(); bls_conf - .with_title(boot_label) + .with_title(boot_label.boot_label) .with_cfg(BLSConfigType::EFI { efi: format!("/{SYSTEMD_UKI_DIR}/{}{}", id.to_hex(), EFI_EXT).into(), }) .with_sort_key(default_sort_key.into()) - // TODO (Johan-Liebert1): Get version from UKI like we get boot label - .with_version(default_sort_key.into()); + .with_version(boot_label.version.unwrap_or(default_sort_key.into())); let (entries_dir, booted_bls) = match setup_type { BootSetupType::Setup(..) => { @@ -827,7 +839,6 @@ fn write_systemd_uki_config( #[context("Setting up UKI boot")] pub(crate) fn setup_composefs_uki_boot( setup_type: BootSetupType, - // TODO: Make this generic repo: crate::store::ComposefsRepository, id: &Sha512HashValue, entries: Vec>, @@ -868,7 +879,7 @@ pub(crate) fn setup_composefs_uki_boot( let esp_mount = mount_esp(&esp_device).context("Mounting ESP")?; - let mut boot_label = String::new(); + let mut uki_label: Option = None; for entry in entries { match entry { @@ -917,20 +928,25 @@ pub(crate) fn setup_composefs_uki_boot( )?; if let Some(label) = ret { - boot_label = label; + uki_label = Some(label); } } }; } + let uki_label = uki_label + .ok_or_else(|| anyhow::anyhow!("Failed to get version and boot label from UKI"))?; + match bootloader { - Bootloader::Grub => { - write_grub_uki_menuentry(root_path, &setup_type, boot_label, id, &esp_device)? - } + Bootloader::Grub => write_grub_uki_menuentry( + root_path, + &setup_type, + uki_label.boot_label, + id, + &esp_device, + )?, - Bootloader::Systemd => { - write_systemd_uki_config(&esp_mount.fd, &setup_type, boot_label, id)? - } + Bootloader::Systemd => write_systemd_uki_config(&esp_mount.fd, &setup_type, uki_label, id)?, }; Ok(()) diff --git a/crates/lib/src/bootc_composefs/status.rs b/crates/lib/src/bootc_composefs/status.rs index 51ab35f83..1f4caa3ef 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -131,7 +131,7 @@ pub(crate) fn get_sorted_type1_boot_entries( /// imgref = transport:image_name #[context("Getting container info")] -async fn get_container_manifest_and_config( +pub(crate) async fn get_container_manifest_and_config( imgref: &String, ) -> Result<(ImageManifest, oci_spec::image::ImageConfiguration)> { let config = containers_image_proxy::ImageProxyConfig::default(); diff --git a/crates/lib/src/bootc_composefs/update.rs b/crates/lib/src/bootc_composefs/update.rs index 018d8f3ed..7ad09f82c 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -1,33 +1,153 @@ use anyhow::{Context, Result}; use camino::Utf8PathBuf; +use composefs::util::{parse_sha256, Sha256Digest}; use fn_error_context::context; +use ostree_ext::oci_spec::image::{ImageConfiguration, ImageManifest}; use crate::{ bootc_composefs::{ boot::{setup_composefs_bls_boot, setup_composefs_uki_boot, BootSetupType, BootType}, - repo::pull_composefs_repo, + repo::{get_imgref, open_composefs_repo, pull_composefs_repo}, service::start_finalize_stated_svc, state::write_composefs_state, - status::composefs_deployment_status, + status::{composefs_deployment_status, get_container_manifest_and_config}, }, cli::UpgradeOpts, + spec::ImageReference, + store::ComposefsRepository, }; +use cap_std_ext::cap_std::{ambient_authority, fs::Dir}; + +#[context("Getting SHA256 Digest for {id}")] +pub fn str_to_sha256digest(id: &str) -> Result { + let id = id.strip_prefix("sha256:").unwrap_or(id); + Ok(parse_sha256(&id)?) +} + +/// Checks if a container image has been pulled to the local composefs repository. +/// +/// This function verifies whether the specified container image exists in the local +/// composefs repository by checking if the image's configuration digest stream is +/// available. It retrieves the image manifest and configuration from the container +/// registry and uses the configuration digest to perform the local availability check. +/// +/// # Arguments +/// +/// * `repo` - The composefs repository +/// * `imgref` - Reference to the container image to check +/// +/// # Returns +/// +/// Returns a tuple containing: +/// * `true` if the image is pulled/available locally, `false` 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)> { + let imgref_repr = get_imgref(&imgref.transport, &imgref.image); + let (manifest, config) = get_container_manifest_and_config(&imgref_repr).await?; + + let img_digest = 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.is_some(), manifest, config)) +} + #[context("Upgrading composefs")] -pub(crate) async fn upgrade_composefs(_opts: UpgradeOpts) -> Result<()> { +pub(crate) async fn upgrade_composefs(opts: UpgradeOpts) -> Result<()> { 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 + let mut imgref = host .spec .image .as_ref() .ok_or_else(|| anyhow::anyhow!("No image source specified"))?; + let sysroot = + Dir::open_ambient_dir("/sysroot", ambient_authority()).context("Opening sysroot")?; + let repo = open_composefs_repo(&sysroot)?; + + 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(()); + } + + // Check if we already have this update staged + let staged_image = host.status.staged.as_ref().and_then(|i| i.image.as_ref()); + + 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 + if staged_image.image_digest == booted_img_digest { + if opts.apply { + return crate::reboot::reboot(); + } + + println!("Update already staged. To apply update run `bootc update --apply`"); + + return Ok(()); + } + + // 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; + + let (img_pulled, staged_manifest, staged_cfg) = is_image_pulled(&repo, 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 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() + ); + } + } + + 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 { @@ -61,5 +181,9 @@ pub(crate) async fn upgrade_composefs(_opts: UpgradeOpts) -> Result<()> { boot_digest, )?; + if opts.apply { + return crate::reboot::reboot(); + } + Ok(()) } diff --git a/crates/lib/src/install/baseline.rs b/crates/lib/src/install/baseline.rs index 7a2e7a148..b20aff31d 100644 --- a/crates/lib/src/install/baseline.rs +++ b/crates/lib/src/install/baseline.rs @@ -36,6 +36,10 @@ use bootc_mount::is_mounted_in_pid1_mountns; // This ensures we end up under 512 to be small-sized. pub(crate) const BOOTPN_SIZE_MB: u32 = 510; pub(crate) const EFIPN_SIZE_MB: u32 = 512; +/// EFI Partition size for composefs installations +/// We need more space than ostree as we have UKIs and UKI addons +/// We might also need to store UKIs for pinned deployments +pub(crate) const CFS_EFIPN_SIZE_MB: u32 = 1024; /// The GPT type for "linux" pub(crate) const LINUX_PARTTYPE: &str = "0FC63DAF-8483-4772-8E79-3D69D8477DE4"; #[cfg(feature = "install-to-disk")] @@ -277,9 +281,16 @@ pub(crate) fn install_create_rootfs( let esp_partno = if super::ARCH_USES_EFI { let esp_guid = crate::discoverable_partition_specification::ESP; partno += 1; + + let esp_size = if state.composefs_options.composefs_backend { + CFS_EFIPN_SIZE_MB + } else { + EFIPN_SIZE_MB + }; + writeln!( &mut partitioning_buf, - r#"size={EFIPN_SIZE_MB}MiB, type={esp_guid}, name="EFI-SYSTEM""# + r#"size={esp_size}MiB, type={esp_guid}, name="EFI-SYSTEM""# )?; Some(partno) } else {