Skip to content

Commit d754a3e

Browse files
committed
deploy: Pull bound images before staging via composefs mount
We were still staging a new bootloader entry even when we failed to pull a LBI. Switch to only doing an image pull and not a deployment, then pulling referenced LBIs from that. Closes: #2013 Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 5476871 commit d754a3e

2 files changed

Lines changed: 85 additions & 23 deletions

File tree

crates/lib/src/cli.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,6 +1210,7 @@ async fn upgrade(
12101210
.ok_or_else(|| anyhow::anyhow!("No staged deployment found"))?;
12111211

12121212
if staged_deployment.is_finalization_locked() {
1213+
crate::boundimage::pull_bound_images(storage, &staged_deployment).await?;
12131214
ostree.change_finalization(&staged_deployment)?;
12141215
println!("Staged deployment will now be applied on reboot");
12151216
} else {

crates/lib/src/deploy.rs

Lines changed: 84 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,6 +1007,80 @@ impl MergeState {
10071007
}
10081008
}
10091009

1010+
/// Mount an ostree commit as a composefs filesystem, returning a read-only
1011+
/// directory handle.
1012+
///
1013+
/// This uses `checkout_composefs` to generate an erofs metadata image from
1014+
/// the commit, then mounts it via composefs with the ostree repo objects
1015+
/// as the backing data store. The returned `Dir` is a detached mount —
1016+
/// it stays alive as long as the fd is open.
1017+
///
1018+
/// The erofs image is written into the ostree repo's own `tmp/` directory
1019+
/// (which lives on the real root filesystem) because erofs file-backed
1020+
/// mounts require a non-stacked backing filesystem.
1021+
#[context("Mounting ostree commit {commit}")]
1022+
pub(crate) fn mount_ostree_commit(repo: &ostree::Repo, commit: &str) -> Result<Dir> {
1023+
use composefs_ctl::composefs::mount::{MountOptions, composefs_fsmount};
1024+
use std::os::fd::AsRawFd;
1025+
1026+
// Write the erofs image into the ostree repo's tmp/ directory so it
1027+
// lives on the real (non-stacked) filesystem.
1028+
let repo_dir = Dir::reopen_dir(&repo.dfd_borrow())?;
1029+
let repo_tmp = repo_dir.open_dir("tmp").context("Opening ostree repo tmp/")?;
1030+
let td = cap_std_ext::cap_tempfile::TempDir::new_in(&repo_tmp)?;
1031+
let image_name = "image.cfs";
1032+
1033+
// Call checkout_composefs via FFI directly; the high-level ostree
1034+
// crate (0.20.x) has a broken feature ladder that omits v2024_7.
1035+
#[allow(unsafe_code)]
1036+
{
1037+
use glib::translate::*;
1038+
use std::ffi::CString;
1039+
let c_path = CString::new(image_name).unwrap();
1040+
let c_commit = CString::new(commit).context("Invalid commit string")?;
1041+
let mut error = std::ptr::null_mut();
1042+
// SAFETY: all pointers are valid; the repo, path, and commit are
1043+
// borrowed for the duration of the call.
1044+
let ok = unsafe {
1045+
ostree::ffi::ostree_repo_checkout_composefs(
1046+
repo.to_glib_none().0,
1047+
std::ptr::null_mut(),
1048+
td.as_raw_fd(),
1049+
c_path.as_ptr(),
1050+
c_commit.as_ptr(),
1051+
std::ptr::null_mut(),
1052+
&mut error,
1053+
)
1054+
};
1055+
if ok == glib::ffi::GFALSE {
1056+
// SAFETY: on failure, error is set by the C function.
1057+
return Err(unsafe { glib::Error::from_glib_full(error) })
1058+
.context("checkout_composefs");
1059+
}
1060+
}
1061+
1062+
// Open the erofs image and the ostree objects directory.
1063+
let image_fd = td
1064+
.open(image_name)
1065+
.context("Opening composefs image")?
1066+
.into();
1067+
let objects_fd = repo_dir
1068+
.open_dir("objects")
1069+
.context("Opening ostree objects dir")?;
1070+
1071+
// Mount via composefs: erofs metadata + ostree objects as data store.
1072+
let mount_fd = composefs_fsmount(
1073+
image_fd,
1074+
commit,
1075+
objects_fd,
1076+
false, // no fsverity requirement for read-only inspection
1077+
&MountOptions::default(),
1078+
)
1079+
.context("composefs_fsmount")?;
1080+
1081+
Dir::reopen_dir(&mount_fd).context("Reopening composefs mount as Dir")
1082+
}
1083+
10101084
/// Stage (queue deployment of) a fetched container image.
10111085
#[context("Staging")]
10121086
pub(crate) async fn stage(
@@ -1072,32 +1146,19 @@ pub(crate) async fn stage(
10721146
.collect(),
10731147
})
10741148
.await;
1149+
// Pull bound images *before* staging by mounting the ostree commit
1150+
// as a composefs filesystem to read the image specs. This way a pull
1151+
// failure never results in a staged deployment at all.
1152+
let repo = sysroot.get_ostree()?.repo();
1153+
let commit_root = mount_ostree_commit(&repo, &image.ostree_commit)?;
1154+
let bound_images = crate::boundimage::query_bound_images(&commit_root)?;
1155+
drop(commit_root);
1156+
crate::boundimage::pull_images(sysroot, bound_images).await?;
1157+
10751158
let origin = origin_from_imageref(spec.image)?;
1076-
let deployment =
1159+
let _deployment =
10771160
crate::deploy::deploy(sysroot, from, image, &origin, lock_finalization).await?;
10781161

1079-
subtask.completed = true;
1080-
subtasks.push(subtask.clone());
1081-
subtask.subtask = "bound_images".into();
1082-
subtask.id = "bound_images".into();
1083-
subtask.description = "Pulling Bound Images".into();
1084-
subtask.completed = false;
1085-
prog.send(Event::ProgressSteps {
1086-
task: "staging".into(),
1087-
description: "Deploying Image".into(),
1088-
id: image.manifest_digest.clone().as_ref().into(),
1089-
steps_cached: 0,
1090-
steps: 1,
1091-
steps_total: 3,
1092-
subtasks: subtasks
1093-
.clone()
1094-
.into_iter()
1095-
.chain([subtask.clone()])
1096-
.collect(),
1097-
})
1098-
.await;
1099-
crate::boundimage::pull_bound_images(sysroot, &deployment).await?;
1100-
11011162
subtask.completed = true;
11021163
subtasks.push(subtask.clone());
11031164
subtask.subtask = "cleanup".into();

0 commit comments

Comments
 (0)