Skip to content

Commit e19a663

Browse files
committed
composefs: Add unified storage pull path
For registry transports, pull_composefs_repo() now goes through bootc-owned containers-storage first (via podman pull), then imports from there into the composefs repo via skopeo's containers-storage transport. This means the source image remains in containers-storage after upgrade, enabling 'podman run <booted-image>'. Non-registry transports (oci:, containers-storage:, docker-daemon:) continue using the direct skopeo path. Also fix composefs_oci::pull() callsite to pass the new zerocopy parameter added in the composefs-rs import-cstor-rs-rebase branch. Assisted-by: OpenCode (Claude Opus 4) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 4951c82 commit e19a663

File tree

11 files changed

+631
-73
lines changed

11 files changed

+631
-73
lines changed

Cargo.lock

Lines changed: 490 additions & 48 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,6 @@ needless_borrow = "allow"
115115
needless_borrows_for_generic_args = "allow"
116116

117117

118-
# Patched by composefs-rs CI to test against local composefs-rs
118+
# Patched by composefs-rs at 6df311cc5a2d7649e76cb98209e481d66f08e383
119119
[patch."https://github.com/composefs/composefs-rs"]
120120
cfsctl = { path = "/workspaces/composefs-rs/crates/cfsctl" } # Patched by composefs-rs

crates/lib/src/bootc_composefs/gc.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,8 @@ pub(crate) async fn composefs_gc(
303303
// deployments that predate the manifest→image link.
304304
let mut live_manifest_digests: Vec<composefs_oci::OciDigest> = Vec::new();
305305
let mut additional_roots = Vec::new();
306+
// Container image names for containers-storage pruning.
307+
let mut live_container_images: std::collections::HashSet<String> = Default::default();
306308

307309
// Read existing tags before the deployment loop so we can search
308310
// them for deployments that lack manifest_digest in their origin.
@@ -324,6 +326,14 @@ pub(crate) async fn composefs_gc(
324326
additional_roots.push(verity.clone());
325327

326328
if let Some(ini) = read_origin(sysroot, verity)? {
329+
// Collect the container image name for containers-storage GC.
330+
if let Some(container_ref) =
331+
ini.get::<String>("origin", ostree_ext::container::deploy::ORIGIN_CONTAINER)
332+
{
333+
let image_name = crate::bootc_composefs::fsck::extract_image_name(&container_ref);
334+
live_container_images.insert(image_name);
335+
}
336+
327337
if let Some(manifest_digest_str) =
328338
ini.get::<String>(ORIGIN_KEY_IMAGE, ORIGIN_KEY_MANIFEST_DIGEST)
329339
{
@@ -407,6 +417,35 @@ pub(crate) async fn composefs_gc(
407417
.map(|x| x.as_str())
408418
.collect::<Vec<_>>();
409419

420+
// Prune containers-storage: remove images not backing any live deployment.
421+
if !dry_run && !live_container_images.is_empty() {
422+
let subpath = crate::podstorage::CStorage::subpath();
423+
if sysroot.try_exists(&subpath).unwrap_or(false) {
424+
match crate::bootc_composefs::fsck::open_imgstore(storage) {
425+
Ok(imgstore) => {
426+
let roots: std::collections::HashSet<&str> =
427+
live_container_images.iter().map(|s| s.as_str()).collect();
428+
match imgstore.prune_except_roots(&roots).await {
429+
Ok(pruned) => {
430+
if !pruned.is_empty() {
431+
tracing::info!(
432+
"Pruned {} images from containers-storage",
433+
pruned.len()
434+
);
435+
}
436+
}
437+
Err(e) => {
438+
tracing::warn!("Failed to prune containers-storage: {e}");
439+
}
440+
}
441+
}
442+
Err(e) => {
443+
tracing::debug!("No containers-storage to prune: {e}");
444+
}
445+
}
446+
}
447+
}
448+
410449
// Run garbage collection. Tags root the OCI metadata chain
411450
// (manifest → config → layers). The additional_roots protect EROFS
412451
// images for deployments that predate the manifest→image link;

crates/lib/src/bootc_composefs/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub(crate) mod delete;
33
pub(crate) mod digest;
44
pub(crate) mod export;
55
pub(crate) mod finalize;
6+
pub(crate) mod fsck;
67
pub(crate) mod gc;
78
pub(crate) mod repo;
89
pub(crate) mod rollback;

crates/lib/src/bootc_composefs/repo.rs

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use fn_error_context::context;
2+
use std::process::Command;
23
use std::sync::Arc;
34

45
use anyhow::{Context, Result};
@@ -19,6 +20,7 @@ use cap_std_ext::cap_std::{ambient_authority, fs::Dir};
1920

2021
use crate::composefs_consts::BOOTC_TAG_PREFIX;
2122
use crate::install::{RootSetup, State};
23+
use crate::podstorage::{CStorage, PullMode};
2224

2325
/// Create a composefs OCI tag name for the given manifest digest.
2426
///
@@ -137,8 +139,81 @@ pub(crate) struct PullRepoResult {
137139
pub(crate) manifest_digest: String,
138140
}
139141

140-
/// Pulls the `image` from `transport` into a composefs repository at /sysroot
141-
/// Checks for boot entries in the image and returns them
142+
/// Pull an image via unified storage: first into bootc-owned containers-storage,
143+
/// then from there into the composefs repository via cstor (zero-copy
144+
/// reflink/hardlink).
145+
///
146+
/// This ensures the image is available in containers-storage for `podman run`
147+
/// while also populating the composefs repo for booting.
148+
async fn pull_composefs_unified(
149+
rootfs_dir: &Dir,
150+
repo: &Arc<crate::store::ComposefsRepository>,
151+
transport: &str,
152+
image: &str,
153+
) -> Result<PullResult<Sha512HashValue>> {
154+
// Open/create bootc-owned containers-storage.
155+
let run = Dir::open_ambient_dir("/run", ambient_authority())?;
156+
let imgstore = CStorage::create(rootfs_dir, &run, None)?;
157+
158+
// Stage 1: get the image into bootc-owned containers-storage.
159+
let t = transport.strip_suffix(':').unwrap_or(transport);
160+
if t == "containers-storage" {
161+
// The image is in the default containers-storage (/var/lib/containers/storage).
162+
// Copy it into bootc-owned storage.
163+
tracing::info!("Unified pull: copying {image} from host containers-storage");
164+
imgstore
165+
.pull_from_host_storage(image)
166+
.await
167+
.context("Copying image from host containers-storage into bootc storage")?;
168+
} else {
169+
// For registry (docker://), oci:, docker-daemon:, etc. — podman
170+
// can pull directly into bootc-owned storage.
171+
let pull_ref = get_imgref(transport, image);
172+
tracing::info!("Unified pull: fetching {pull_ref} into containers-storage");
173+
imgstore
174+
.pull(&pull_ref, PullMode::Always)
175+
.await
176+
.context("Pulling image into bootc containers-storage")?;
177+
}
178+
179+
// Stage 2: import layers+config from containers-storage into composefs
180+
// via cstor (zero-copy reflink/hardlink from overlay diff/).
181+
let cstor_imgref = format!("containers-storage:{image}");
182+
tracing::info!("Unified pull: importing layers from {cstor_imgref} (zero-copy)");
183+
184+
let cstor_result = composefs_oci::pull(repo, &cstor_imgref, None, None, false)
185+
.await
186+
.context("Importing layers from containers-storage into composefs")?;
187+
188+
tracing::info!(
189+
"Unified pull: cstor import complete (config {}), importing manifest",
190+
cstor_result.config_digest
191+
);
192+
193+
// Stage 3: fetch and store the OCI manifest via skopeo.
194+
// The layers+config are already in composefs (imported above), so skopeo
195+
// will see them as AlreadyPresent and only create the manifest splitstream.
196+
let storage_path = format!("/sysroot/{}", CStorage::subpath());
197+
let mut config = crate::deploy::new_proxy_config();
198+
ostree_ext::container::merge_default_container_proxy_opts(&mut config)?;
199+
let mut cmd = Command::new("skopeo");
200+
crate::podstorage::set_additional_image_store(&mut cmd, &storage_path);
201+
config.skopeo_cmd = Some(cmd);
202+
203+
let (pull_result, _stats) = composefs_oci_pull_image(repo, &cstor_imgref, None, Some(config))
204+
.await
205+
.context("Storing manifest from containers-storage")?;
206+
207+
Ok(pull_result)
208+
}
209+
210+
/// Pulls the `image` from `transport` into a composefs repository at /sysroot.
211+
///
212+
/// For registry transports, this uses the unified storage path: the image is
213+
/// first pulled into bootc-owned containers-storage (so it's available for
214+
/// `podman run`), then imported from there into the composefs repo.
215+
///
216+
/// Checks for boot entries in the image and returns them.
142217
#[context("Pulling composefs repository")]
143218
pub(crate) async fn pull_composefs_repo(
144219
transport: &String,
@@ -165,17 +240,9 @@ pub(crate) async fn pull_composefs_repo(
165240
repo.set_insecure();
166241
}
167242

168-
let final_imgref = get_imgref(transport, image);
169-
170-
tracing::debug!("Image to pull {final_imgref}");
171-
172-
let mut config = crate::deploy::new_proxy_config();
173-
ostree_ext::container::merge_default_container_proxy_opts(&mut config)?;
174-
175243
let repo = Arc::new(repo);
176-
let (pull_result, _stats) = composefs_oci_pull_image(&repo, &final_imgref, None, Some(config))
177-
.await
178-
.context("Pulling composefs repo")?;
244+
245+
let pull_result = pull_composefs_unified(&rootfs_dir, &repo, transport, image).await?;
179246

180247
// Tag the manifest as a bootc-owned GC root.
181248
let tag = bootc_tag_for_manifest(&pull_result.manifest_digest.to_string());

crates/lib/src/cli.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1748,7 +1748,7 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
17481748
};
17491749

17501750
let imgref = format!("containers-storage:{image}");
1751-
let pull_result = composefs_oci::pull(&repo, &imgref, None, Some(proxycfg))
1751+
let pull_result = composefs_oci::pull(&repo, &imgref, None, Some(proxycfg), false)
17521752
.await
17531753
.context("Pulling image")?;
17541754
let mut fs = composefs_oci::image::create_filesystem(

crates/lib/src/fsck.rs

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,20 @@ use std::os::fd::AsFd;
2828

2929
/// A lint check has failed.
3030
#[derive(thiserror::Error, Debug)]
31-
struct FsckError(String);
31+
pub(crate) struct FsckError(String);
3232

3333
/// The outer error is for unexpected fatal runtime problems; the
3434
/// inner error is for the check failing in an expected way.
35-
type FsckResult = anyhow::Result<std::result::Result<(), FsckError>>;
35+
pub(crate) type FsckResult = anyhow::Result<std::result::Result<(), FsckError>>;
3636

3737
/// Everything is OK - we didn't encounter a runtime error, and
3838
/// the targeted check passed.
39-
fn fsck_ok() -> FsckResult {
39+
pub(crate) fn fsck_ok() -> FsckResult {
4040
Ok(Ok(()))
4141
}
4242

4343
/// We successfully found a failure.
44-
fn fsck_err(msg: impl AsRef<str>) -> FsckResult {
44+
pub(crate) fn fsck_err(msg: impl AsRef<str>) -> FsckResult {
4545
Ok(Err(FsckError::new(msg)))
4646
}
4747

@@ -57,10 +57,10 @@ impl FsckError {
5757
}
5858
}
5959

60-
type FsckFn = fn(&Storage) -> FsckResult;
61-
type AsyncFsckFn = fn(&Storage) -> Pin<Box<dyn Future<Output = FsckResult> + '_>>;
60+
pub(crate) type FsckFn = fn(&Storage) -> FsckResult;
61+
pub(crate) type AsyncFsckFn = fn(&Storage) -> Pin<Box<dyn Future<Output = FsckResult> + '_>>;
6262
#[derive(Debug)]
63-
enum FsckFnImpl {
63+
pub(crate) enum FsckFnImpl {
6464
Sync(FsckFn),
6565
Async(AsyncFsckFn),
6666
}
@@ -78,7 +78,7 @@ impl From<AsyncFsckFn> for FsckFnImpl {
7878
}
7979

8080
#[derive(Debug)]
81-
struct FsckCheck {
81+
pub(crate) struct FsckCheck {
8282
name: &'static str,
8383
ordering: u16,
8484
f: FsckFnImpl,
@@ -106,7 +106,10 @@ static CHECK_RESOLVCONF: FsckCheck =
106106
/// But at the current time fsck is an experimental feature that we should only be running
107107
/// in our CI.
108108
fn check_resolvconf(storage: &Storage) -> FsckResult {
109-
let ostree = storage.get_ostree()?;
109+
let ostree = match storage.get_ostree() {
110+
Ok(o) => o,
111+
Err(_) => return fsck_ok(), // Not an ostree system (e.g. composefs-only)
112+
};
110113
// For now we only check the booted deployment.
111114
if ostree.booted_deployment().is_none() {
112115
return fsck_ok();
@@ -232,7 +235,10 @@ fn check_fsverity(storage: &Storage) -> Pin<Box<dyn Future<Output = FsckResult>
232235
}
233236

234237
async fn check_fsverity_inner(storage: &Storage) -> FsckResult {
235-
let ostree = storage.get_ostree()?;
238+
let ostree = match storage.get_ostree() {
239+
Ok(o) => o,
240+
Err(_) => return fsck_ok(), // Not an ostree system (e.g. composefs-only)
241+
};
236242
let repo = &ostree.repo();
237243
let verity_state = ostree_ext::fsverity::is_verity_enabled(repo)?;
238244
tracing::debug!(

crates/lib/src/generator.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ pub(crate) fn generator(root: &Dir, unit_dir: &Dir) -> Result<()> {
108108
tracing::trace!("Root is writable");
109109
return Ok(());
110110
}
111+
111112
let updated = fstab_generator_impl(root, unit_dir)?;
112113
tracing::trace!("Generated fstab: {updated}");
113114

crates/lib/src/install.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ use self::baseline::InstallBlockDeviceOpts;
190190
use crate::bootc_composefs::status::ComposefsCmdline;
191191
use crate::bootc_composefs::{
192192
boot::setup_composefs_boot,
193-
repo::{get_imgref, initialize_composefs_repository, open_composefs_repo},
193+
repo::{get_imgref, initialize_composefs_repository},
194194
status::get_container_manifest_and_config,
195195
};
196196
use crate::boundimage::{BoundImage, ResolvedBoundImage};

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const quoted_karg = '"thisarg=quoted with spaces"'
2020
# This code runs on *each* boot.
2121
# Here we just capture information.
2222
bootc status
23+
bootc internals fsck
2324
let st = bootc status --json | from json
2425
let booted = $st.status.booted.image
2526
let is_composefs = (tap is_composefs)

0 commit comments

Comments
 (0)