Skip to content

Commit d492c0b

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. The "bound_images" progress subtask moves earlier in the sequence accordingly, ahead of "deploying" instead of after it, since the pull now happens before deploy() is called. Closes: #2013 Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 220f404 commit d492c0b

3 files changed

Lines changed: 98 additions & 11 deletions

File tree

crates/lib/src/cli.rs

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

12461246
if staged_deployment.is_finalization_locked() {
1247+
crate::boundimage::pull_bound_images(storage, &staged_deployment).await?;
12471248
ostree.change_finalization(&staged_deployment)?;
12481249
println!("Staged deployment will now be applied on reboot");
12491250
} else {

crates/lib/src/deploy.rs

Lines changed: 93 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,6 +1007,82 @@ 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
1030+
.open_dir("tmp")
1031+
.context("Opening ostree repo tmp/")?;
1032+
let td = cap_std_ext::cap_tempfile::TempDir::new_in(&repo_tmp)?;
1033+
let image_name = "image.cfs";
1034+
1035+
// Call checkout_composefs via FFI directly; the high-level ostree
1036+
// crate (0.20.x) has a broken feature ladder that omits v2024_7.
1037+
#[allow(unsafe_code)]
1038+
{
1039+
use glib::translate::*;
1040+
use std::ffi::CString;
1041+
let c_path = CString::new(image_name).unwrap();
1042+
let c_commit = CString::new(commit).context("Invalid commit string")?;
1043+
let mut error = std::ptr::null_mut();
1044+
// SAFETY: all pointers are valid; the repo, path, and commit are
1045+
// borrowed for the duration of the call.
1046+
let ok = unsafe {
1047+
ostree::ffi::ostree_repo_checkout_composefs(
1048+
repo.to_glib_none().0,
1049+
std::ptr::null_mut(),
1050+
td.as_raw_fd(),
1051+
c_path.as_ptr(),
1052+
c_commit.as_ptr(),
1053+
std::ptr::null_mut(),
1054+
&mut error,
1055+
)
1056+
};
1057+
if ok == glib::ffi::GFALSE {
1058+
// SAFETY: on failure, error is set by the C function.
1059+
return Err(unsafe { glib::Error::from_glib_full(error) })
1060+
.context("checkout_composefs");
1061+
}
1062+
}
1063+
1064+
// Open the erofs image and the ostree objects directory.
1065+
let image_fd = td
1066+
.open(image_name)
1067+
.context("Opening composefs image")?
1068+
.into();
1069+
let objects_fd = repo_dir
1070+
.open_dir("objects")
1071+
.context("Opening ostree objects dir")?;
1072+
1073+
// Mount via composefs: erofs metadata + ostree objects as data store.
1074+
let mount_fd = composefs_fsmount(
1075+
image_fd,
1076+
commit,
1077+
objects_fd,
1078+
false, // no fsverity requirement for read-only inspection
1079+
&MountOptions::default(),
1080+
)
1081+
.context("composefs_fsmount")?;
1082+
1083+
Dir::reopen_dir(&mount_fd).context("Reopening composefs mount as Dir")
1084+
}
1085+
10101086
/// Stage (queue deployment of) a fetched container image.
10111087
#[context("Staging")]
10121088
pub(crate) async fn stage(
@@ -1054,9 +1130,9 @@ pub(crate) async fn stage(
10541130

10551131
subtask.completed = true;
10561132
subtasks.push(subtask.clone());
1057-
subtask.subtask = "deploying".into();
1058-
subtask.id = "deploying".into();
1059-
subtask.description = "Deploying Image".into();
1133+
subtask.subtask = "bound_images".into();
1134+
subtask.id = "bound_images".into();
1135+
subtask.description = "Pulling Bound Images".into();
10601136
subtask.completed = false;
10611137
prog.send(Event::ProgressSteps {
10621138
task: "staging".into(),
@@ -1072,15 +1148,20 @@ pub(crate) async fn stage(
10721148
.collect(),
10731149
})
10741150
.await;
1075-
let origin = origin_from_imageref(spec.image)?;
1076-
let deployment =
1077-
crate::deploy::deploy(sysroot, from, image, &origin, lock_finalization).await?;
1151+
// Pull bound images *before* staging by mounting the ostree commit
1152+
// as a composefs filesystem to read the image specs. This way a pull
1153+
// failure never results in a staged deployment at all.
1154+
let repo = sysroot.get_ostree()?.repo();
1155+
let commit_root = mount_ostree_commit(&repo, &image.ostree_commit)?;
1156+
let bound_images = crate::boundimage::query_bound_images(&commit_root)?;
1157+
drop(commit_root);
1158+
crate::boundimage::pull_images(sysroot, bound_images).await?;
10781159

10791160
subtask.completed = true;
10801161
subtasks.push(subtask.clone());
1081-
subtask.subtask = "bound_images".into();
1082-
subtask.id = "bound_images".into();
1083-
subtask.description = "Pulling Bound Images".into();
1162+
subtask.subtask = "deploying".into();
1163+
subtask.id = "deploying".into();
1164+
subtask.description = "Deploying Image".into();
10841165
subtask.completed = false;
10851166
prog.send(Event::ProgressSteps {
10861167
task: "staging".into(),
@@ -1096,7 +1177,9 @@ pub(crate) async fn stage(
10961177
.collect(),
10971178
})
10981179
.await;
1099-
crate::boundimage::pull_bound_images(sysroot, &deployment).await?;
1180+
let origin = origin_from_imageref(spec.image)?;
1181+
let _deployment =
1182+
crate::deploy::deploy(sysroot, from, image, &origin, lock_finalization).await?;
11001183

11011184
subtask.completed = true;
11021185
subtasks.push(subtask.clone());

tmt/tests/booted/test-image-pushpull-upgrade.nu

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,12 @@ def sanity_check_switch_progress_json [data] {
130130
assert equal $deploy.steps 3
131131
assert equal $deploy.stepsTotal 3
132132
let deploy_tasks = $deploy.subtasks
133+
# Bound images are now pulled before staging (see deploy::stage), so
134+
# the "bound_images" subtask now comes before "deploying" instead of
135+
# after it.
133136
assert equal ($deploy_tasks | length) 5
134137
let deploy_names = $deploy_tasks | get subtask
135-
assert equal $deploy_names ["merging", "deploying", "bound_images", "cleanup", "cleanup"]
138+
assert equal $deploy_names ["merging", "bound_images", "deploying", "cleanup", "cleanup"]
136139
}
137140

138141
# The second boot; verify we're in the derived image

0 commit comments

Comments
 (0)