Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 36 additions & 20 deletions crates/lib/src/bootc_composefs/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Sha512HashValue>,
Expand Down Expand Up @@ -563,6 +565,11 @@ pub(crate) fn setup_composefs_bls_boot(
Ok(boot_digest)
}

struct UKILabels {
boot_label: String,
version: Option<String>,
}

/// Writes a PortableExecutable to ESP along with any PE specific or Global addons
#[context("Writing {file_path} to ESP")]
fn write_pe_to_esp(
Expand All @@ -574,10 +581,10 @@ fn write_pe_to_esp(
is_insecure_from_opts: bool,
mounted_efi: impl AsRef<Path>,
bootloader: &Bootloader,
) -> Result<Option<String>> {
) -> Result<Option<UKILabels>> {
let efi_bin = read_file(file, &repo).context("Reading .efi binary")?;

let mut boot_label = None;
let mut boot_label: Option<UKILabels> = None;

// UKI Extension might not even have a cmdline
// TODO: UKI Addon might also have a composefs= cmdline?
Expand Down Expand Up @@ -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")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes...though we should also have exactly this same data in /usr/lib/os-release in the target image right? My inclination would be to use that over parsing the UKI, but in the end it doesn't really matter.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think we could've gone either way as the data from /usr/lib/os-release will be embedded in the UKI anyway

.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
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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(..) => {
Expand Down Expand Up @@ -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<ComposefsBootEntry<Sha512HashValue>>,
Expand Down Expand Up @@ -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<UKILabels> = None;

for entry in entries {
match entry {
Expand Down Expand Up @@ -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(())
Expand Down
2 changes: 1 addition & 1 deletion crates/lib/src/bootc_composefs/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
138 changes: 131 additions & 7 deletions crates/lib/src/bootc_composefs/update.rs
Original file line number Diff line number Diff line change
@@ -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<Sha256Digest> {
let id = id.strip_prefix("sha256:").unwrap_or(id);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm it feels like being "loose" in our inputs is not a good idea; we should either strictly require a digest prefix or not.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. This was just me being careful. I think the layer ids always start with sha256:

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?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think we need to only write the config object after all of its dependent layers.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what we do. This comment was for the cases where we pull the entire image and create an erofs image, but fail to create some files in the state dir, or maybe something else. The bootloader entries will be overwritten, but we will fail the update if the state directory already exists.

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And yes we need to store the manifest.

// 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 {
Expand Down Expand Up @@ -61,5 +181,9 @@ pub(crate) async fn upgrade_composefs(_opts: UpgradeOpts) -> Result<()> {
boot_digest,
)?;

if opts.apply {
return crate::reboot::reboot();
}

Ok(())
}
13 changes: 12 additions & 1 deletion crates/lib/src/install/baseline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Ultimately of course we want this to be more OS controllable, and that thread is adding support for repart.d in to-disk)

/// The GPT type for "linux"
pub(crate) const LINUX_PARTTYPE: &str = "0FC63DAF-8483-4772-8E79-3D69D8477DE4";
#[cfg(feature = "install-to-disk")]
Expand Down Expand Up @@ -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 {
Expand Down