-
Notifications
You must be signed in to change notification settings - Fork 156
Composefs native refactor #1599
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
cgwalters
merged 3 commits into
bootc-dev:composefs-backend
from
Johan-Liebert1:composefs-native-refactor
Sep 9, 2025
Merged
Changes from 2 commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,7 @@ | ||
| pub(crate) mod boot; | ||
| pub(crate) mod repo; | ||
| pub(crate) mod rollback; | ||
| pub(crate) mod state; | ||
| pub(crate) mod status; | ||
| pub(crate) mod switch; | ||
| pub(crate) mod update; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| use fn_error_context::context; | ||
| use std::sync::Arc; | ||
|
|
||
| use anyhow::{Context, Result}; | ||
|
|
||
| use ostree_ext::composefs::{ | ||
| fsverity::{FsVerityHashValue, Sha256HashValue}, | ||
| repository::Repository as ComposefsRepository, | ||
| tree::FileSystem, | ||
| util::Sha256Digest, | ||
| }; | ||
| use ostree_ext::composefs_boot::{bootloader::BootEntry as ComposefsBootEntry, BootOps}; | ||
| use ostree_ext::composefs_oci::{ | ||
| image::create_filesystem as create_composefs_filesystem, pull as composefs_oci_pull, | ||
| }; | ||
|
|
||
| use ostree_ext::container::ImageReference as OstreeExtImgRef; | ||
|
|
||
| use cap_std_ext::cap_std::{ambient_authority, fs::Dir}; | ||
|
|
||
| use crate::install::{RootSetup, State}; | ||
|
|
||
| pub(crate) fn open_composefs_repo( | ||
| rootfs_dir: &Dir, | ||
| ) -> Result<ComposefsRepository<Sha256HashValue>> { | ||
| ComposefsRepository::open_path(rootfs_dir, "composefs") | ||
| .context("Failed to open composefs repository") | ||
| } | ||
|
|
||
| pub(crate) async fn initialize_composefs_repository( | ||
| state: &State, | ||
| root_setup: &RootSetup, | ||
| ) -> Result<(Sha256Digest, impl FsVerityHashValue)> { | ||
| let rootfs_dir = &root_setup.physical_root; | ||
|
|
||
| rootfs_dir | ||
| .create_dir_all("composefs") | ||
| .context("Creating dir composefs")?; | ||
|
|
||
| let repo = open_composefs_repo(rootfs_dir)?; | ||
|
|
||
| let OstreeExtImgRef { | ||
| name: image_name, | ||
| transport, | ||
| } = &state.source.imageref; | ||
|
|
||
| // transport's display is already of type "<transport_type>:" | ||
| composefs_oci_pull( | ||
| &Arc::new(repo), | ||
| &format!("{transport}{image_name}"), | ||
| None, | ||
| None, | ||
| ) | ||
| .await | ||
| } | ||
|
|
||
| /// Pulls the `image` from `transport` into a composefs repository at /sysroot | ||
| /// Checks for boot entries in the image and returns them | ||
| #[context("Pulling composefs repository")] | ||
| pub(crate) async fn pull_composefs_repo( | ||
| transport: &String, | ||
| image: &String, | ||
| ) -> Result<( | ||
| ComposefsRepository<Sha256HashValue>, | ||
| Vec<ComposefsBootEntry<Sha256HashValue>>, | ||
| Sha256HashValue, | ||
| FileSystem<Sha256HashValue>, | ||
| )> { | ||
| let rootfs_dir = Dir::open_ambient_dir("/sysroot", ambient_authority())?; | ||
|
|
||
| let repo = open_composefs_repo(&rootfs_dir).context("Opening compoesfs repo")?; | ||
|
|
||
| let (id, verity) = | ||
| composefs_oci_pull(&Arc::new(repo), &format!("{transport}:{image}"), None, None) | ||
| .await | ||
| .context("Pulling composefs repo")?; | ||
|
|
||
| tracing::debug!( | ||
| "id = {id}, verity = {verity}", | ||
| id = hex::encode(id), | ||
| verity = verity.to_hex() | ||
| ); | ||
|
|
||
| let repo = open_composefs_repo(&rootfs_dir)?; | ||
| let mut fs = create_composefs_filesystem(&repo, &hex::encode(id), None) | ||
| .context("Failed to create composefs filesystem")?; | ||
|
|
||
| let entries = fs.transform_for_boot(&repo)?; | ||
| let id = fs.commit_image(&repo, None)?; | ||
|
|
||
| Ok((repo, entries, id, fs)) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| use std::path::PathBuf; | ||
| use std::{fmt::Write, fs::create_dir_all}; | ||
|
|
||
| use anyhow::{anyhow, Context, Result}; | ||
| use cap_std_ext::cap_std::fs::Dir; | ||
| use cap_std_ext::{cap_std, dirext::CapStdExtDirExt}; | ||
| use fn_error_context::context; | ||
| use rustix::fs::{fsync, renameat_with, AtFlags, RenameFlags}; | ||
|
|
||
| use crate::bootc_composefs::boot::BootType; | ||
| use crate::bootc_composefs::status::{composefs_deployment_status, get_sorted_bls_boot_entries}; | ||
| use crate::{ | ||
| bootc_composefs::{boot::get_efi_uuid_source, status::get_sorted_uki_boot_entries}, | ||
| composefs_consts::{ | ||
| BOOT_LOADER_ENTRIES, ROLLBACK_BOOT_LOADER_ENTRIES, USER_CFG, USER_CFG_ROLLBACK, | ||
| }, | ||
| spec::BootOrder, | ||
| }; | ||
|
|
||
| #[context("Rolling back UKI")] | ||
| pub(crate) fn rollback_composefs_uki() -> Result<()> { | ||
| let user_cfg_path = PathBuf::from("/sysroot/boot/grub2"); | ||
|
|
||
| let mut str = String::new(); | ||
| let boot_dir = | ||
| cap_std::fs::Dir::open_ambient_dir("/sysroot/boot", cap_std::ambient_authority()) | ||
| .context("Opening boot dir")?; | ||
| let mut menuentries = | ||
| get_sorted_uki_boot_entries(&boot_dir, &mut str).context("Getting UKI boot entries")?; | ||
|
|
||
| // TODO(Johan-Liebert): Currently assuming there are only two deployments | ||
| assert!(menuentries.len() == 2); | ||
|
|
||
| let (first, second) = menuentries.split_at_mut(1); | ||
| std::mem::swap(&mut first[0], &mut second[0]); | ||
|
|
||
| let mut buffer = get_efi_uuid_source(); | ||
|
|
||
| for entry in menuentries { | ||
| write!(buffer, "{entry}")?; | ||
| } | ||
|
|
||
| let entries_dir = | ||
| cap_std::fs::Dir::open_ambient_dir(&user_cfg_path, cap_std::ambient_authority()) | ||
| .with_context(|| format!("Opening {user_cfg_path:?}"))?; | ||
|
|
||
| entries_dir | ||
| .atomic_write(USER_CFG_ROLLBACK, buffer) | ||
| .with_context(|| format!("Writing to {USER_CFG_ROLLBACK}"))?; | ||
|
|
||
| tracing::debug!("Atomically exchanging for {USER_CFG_ROLLBACK} and {USER_CFG}"); | ||
| renameat_with( | ||
| &entries_dir, | ||
| USER_CFG_ROLLBACK, | ||
| &entries_dir, | ||
| USER_CFG, | ||
| RenameFlags::EXCHANGE, | ||
| ) | ||
| .context("renameat")?; | ||
|
|
||
| tracing::debug!("Removing {USER_CFG_ROLLBACK}"); | ||
| rustix::fs::unlinkat(&entries_dir, USER_CFG_ROLLBACK, AtFlags::empty()).context("unlinkat")?; | ||
|
|
||
| tracing::debug!("Syncing to disk"); | ||
| fsync( | ||
| entries_dir | ||
| .reopen_as_ownedfd() | ||
| .with_context(|| format!("Reopening {user_cfg_path:?} as owned fd"))?, | ||
| ) | ||
| .with_context(|| format!("fsync {user_cfg_path:?}"))?; | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| #[context("Rolling back BLS")] | ||
| pub(crate) fn rollback_composefs_bls() -> Result<()> { | ||
| let boot_dir = | ||
| cap_std::fs::Dir::open_ambient_dir("/sysroot/boot", cap_std::ambient_authority()) | ||
| .context("Opening boot dir")?; | ||
|
|
||
| // Sort in descending order as that's the order they're shown on the boot screen | ||
| // After this: | ||
| // all_configs[0] -> booted depl | ||
| // all_configs[1] -> rollback depl | ||
| let mut all_configs = get_sorted_bls_boot_entries(&boot_dir, false)?; | ||
|
|
||
| // Update the indicies so that they're swapped | ||
| for (idx, cfg) in all_configs.iter_mut().enumerate() { | ||
| cfg.sort_key = Some(idx.to_string()); | ||
| } | ||
|
|
||
| // TODO(Johan-Liebert): Currently assuming there are only two deployments | ||
| assert!(all_configs.len() == 2); | ||
|
|
||
| // Write these | ||
| let dir_path = PathBuf::from(format!( | ||
| "/sysroot/boot/loader/{ROLLBACK_BOOT_LOADER_ENTRIES}", | ||
| )); | ||
| create_dir_all(&dir_path).with_context(|| format!("Failed to create dir: {dir_path:?}"))?; | ||
|
|
||
| let rollback_entries_dir = | ||
| cap_std::fs::Dir::open_ambient_dir(&dir_path, cap_std::ambient_authority()) | ||
| .with_context(|| format!("Opening {dir_path:?}"))?; | ||
|
|
||
| // Write the BLS configs in there | ||
| for cfg in all_configs { | ||
| // SAFETY: We set sort_key above | ||
| let file_name = format!("bootc-composefs-{}.conf", cfg.sort_key.as_ref().unwrap()); | ||
|
|
||
| rollback_entries_dir | ||
| .atomic_write(&file_name, cfg.to_string()) | ||
| .with_context(|| format!("Writing to {file_name}"))?; | ||
| } | ||
|
|
||
| // Should we sync after every write? | ||
| fsync( | ||
| rollback_entries_dir | ||
| .reopen_as_ownedfd() | ||
| .with_context(|| format!("Reopening {dir_path:?} as owned fd"))?, | ||
| ) | ||
| .with_context(|| format!("fsync {dir_path:?}"))?; | ||
|
|
||
| // Atomically exchange "entries" <-> "entries.rollback" | ||
| let dir = Dir::open_ambient_dir("/sysroot/boot/loader", cap_std::ambient_authority()) | ||
| .context("Opening loader dir")?; | ||
|
|
||
| tracing::debug!( | ||
| "Atomically exchanging for {ROLLBACK_BOOT_LOADER_ENTRIES} and {BOOT_LOADER_ENTRIES}" | ||
| ); | ||
| renameat_with( | ||
| &dir, | ||
| ROLLBACK_BOOT_LOADER_ENTRIES, | ||
| &dir, | ||
| BOOT_LOADER_ENTRIES, | ||
| RenameFlags::EXCHANGE, | ||
| ) | ||
| .context("renameat")?; | ||
|
|
||
| tracing::debug!("Removing {ROLLBACK_BOOT_LOADER_ENTRIES}"); | ||
| rustix::fs::unlinkat(&dir, ROLLBACK_BOOT_LOADER_ENTRIES, AtFlags::empty()) | ||
| .context("unlinkat")?; | ||
|
|
||
| tracing::debug!("Syncing to disk"); | ||
| fsync( | ||
| dir.reopen_as_ownedfd() | ||
| .with_context(|| format!("Reopening /sysroot/boot/loader as owned fd"))?, | ||
| ) | ||
| .context("fsync")?; | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| #[context("Rolling back composefs")] | ||
| pub(crate) async fn composefs_rollback() -> Result<()> { | ||
| let host = composefs_deployment_status().await?; | ||
|
|
||
| let new_spec = { | ||
| let mut new_spec = host.spec.clone(); | ||
| new_spec.boot_order = new_spec.boot_order.swap(); | ||
| new_spec | ||
| }; | ||
|
|
||
| // Just to be sure | ||
| host.spec.verify_transition(&new_spec)?; | ||
|
|
||
| let reverting = new_spec.boot_order == BootOrder::Default; | ||
| if reverting { | ||
| println!("notice: Reverting queued rollback state"); | ||
| } | ||
|
|
||
| let rollback_status = host | ||
| .status | ||
| .rollback | ||
| .ok_or_else(|| anyhow!("No rollback available"))?; | ||
|
|
||
| // TODO: Handle staged deployment | ||
| // Ostree will drop any staged deployment on rollback but will keep it if it is the first item | ||
| // in the new deployment list | ||
| let Some(rollback_composefs_entry) = &rollback_status.composefs else { | ||
| anyhow::bail!("Rollback deployment not a composefs deployment") | ||
| }; | ||
|
|
||
| match rollback_composefs_entry.boot_type { | ||
| BootType::Bls => rollback_composefs_bls(), | ||
| BootType::Uki => rollback_composefs_uki(), | ||
| }?; | ||
|
|
||
| if reverting { | ||
| println!("Next boot: current deployment"); | ||
| } else { | ||
| println!("Next boot: rollback deployment"); | ||
| } | ||
|
|
||
| Ok(()) | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To avoid opening the composefs repository twice, you can create an
Arcfor the repository and reuse it. This is slightly more efficient and makes the code cleaner. This change also fixes a typo in the error message on line 71.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We return the repo right now and most functions accept a non Arc repo