Skip to content

Commit 54307c3

Browse files
committed
composefs: Update to latest, 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 cstor (zero-copy reflink/hardlink). 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. Clean up GC to use CStorage::create() directly instead of going through storage.get_ensure_imgstore() which requires ostree and fails on composefs-only systems. Remove the unreferenced fsck module. Assisted-by: OpenCode (Claude Opus 4) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 5ed396e commit 54307c3

File tree

18 files changed

+407
-137
lines changed

18 files changed

+407
-137
lines changed

crates/lib/src/bootc_composefs/boot.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1198,7 +1198,7 @@ fn get_secureboot_keys(fs: &Dir, p: &str) -> Result<Option<SecurebootKeys>> {
11981198
pub(crate) async fn setup_composefs_boot(
11991199
root_setup: &RootSetup,
12001200
state: &State,
1201-
pull_result: &composefs_oci::skopeo::PullResult<Sha512HashValue>,
1201+
pull_result: &composefs_oci::PullResult<Sha512HashValue>,
12021202
allow_missing_fsverity: bool,
12031203
) -> Result<()> {
12041204
const COMPOSEFS_BOOT_SETUP_JOURNAL_ID: &str = "1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5";

crates/lib/src/bootc_composefs/digest.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ use cfsctl::composefs_boot;
1414
use composefs::dumpfile;
1515
use composefs::fsverity::{Algorithm, FsVerityHashValue};
1616
use composefs_boot::BootOps as _;
17-
use rustix::fd::AsFd;
1817
use tempfile::TempDir;
1918

2019
use crate::store::ComposefsRepository;
@@ -68,13 +67,19 @@ pub(crate) async fn compute_composefs_digest(
6867
let (_td_guard, repo) = new_temp_composefs_repo()?;
6968

7069
// Read filesystem from path, transform for boot, compute digest
71-
let cwd_owned: OwnedFd = rustix::fs::CWD.as_fd().try_clone_to_owned()?;
70+
let dirfd: OwnedFd = rustix::fs::open(
71+
path.as_std_path(),
72+
rustix::fs::OFlags::RDONLY | rustix::fs::OFlags::DIRECTORY | rustix::fs::OFlags::CLOEXEC,
73+
rustix::fs::Mode::empty(),
74+
)
75+
.with_context(|| format!("Opening {path}"))?;
7276
let mut fs = composefs::fs::read_container_root(
73-
cwd_owned,
74-
path.as_std_path().to_path_buf(),
77+
dirfd,
78+
std::path::PathBuf::from("."),
7579
Some(repo.clone()),
7680
)
77-
.await?;
81+
.await
82+
.context("Reading container root")?;
7883
fs.transform_for_boot(&repo).context("Preparing for boot")?;
7984
let id = fs.compute_image_id();
8085
let digest = id.to_hex();
@@ -122,7 +127,7 @@ mod tests {
122127
Ok(())
123128
}
124129

125-
#[tokio::test]
130+
#[tokio::test(flavor = "multi_thread")]
126131
async fn test_compute_composefs_digest() {
127132
// Create temp directory with test filesystem structure
128133
let td = tempfile::tempdir().unwrap();

crates/lib/src/bootc_composefs/gc.rs

Lines changed: 30 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,19 @@ 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+
// Parse the ostree image reference to extract the bare image name
334+
// (e.g. "quay.io/foo:tag" from "ostree-unverified-image:docker://quay.io/foo:tag")
335+
let image_name = container_ref
336+
.parse::<ostree_ext::container::OstreeImageReference>()
337+
.map(|r| r.imgref.name)
338+
.unwrap_or_else(|_| container_ref.clone());
339+
live_container_images.insert(image_name);
340+
}
341+
327342
if let Some(manifest_digest_str) =
328343
ini.get::<String>(ORIGIN_KEY_IMAGE, ORIGIN_KEY_MANIFEST_DIGEST)
329344
{
@@ -407,6 +422,21 @@ pub(crate) async fn composefs_gc(
407422
.map(|x| x.as_str())
408423
.collect::<Vec<_>>();
409424

425+
// Prune containers-storage: remove images not backing any live deployment.
426+
if !dry_run && !live_container_images.is_empty() {
427+
let subpath = crate::podstorage::CStorage::subpath();
428+
if sysroot.try_exists(&subpath).unwrap_or(false) {
429+
let run = Dir::open_ambient_dir("/run", cap_std_ext::cap_std::ambient_authority())?;
430+
let imgstore = crate::podstorage::CStorage::create(&sysroot, &run, None)?;
431+
let roots: std::collections::HashSet<&str> =
432+
live_container_images.iter().map(|s| s.as_str()).collect();
433+
let pruned = imgstore.prune_except_roots(&roots).await?;
434+
if !pruned.is_empty() {
435+
tracing::info!("Pruned {} images from containers-storage", pruned.len());
436+
}
437+
}
438+
}
439+
410440
// Run garbage collection. Tags root the OCI metadata chain
411441
// (manifest → config → layers). The additional_roots protect EROFS
412442
// images for deployments that predate the manifest→image link;

crates/lib/src/bootc_composefs/repo.rs

Lines changed: 144 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,17 @@ use cfsctl::composefs_oci;
99
use composefs::fsverity::{FsVerityHashValue, Sha512HashValue};
1010
use composefs_boot::bootloader::{BootEntry as ComposefsBootEntry, get_boot_resources};
1111
use composefs_oci::{
12-
image::create_filesystem as create_composefs_filesystem,
13-
pull_image as composefs_oci_pull_image, skopeo::PullResult, tag_image,
12+
PullOptions, PullResult, image::create_filesystem as create_composefs_filesystem, tag_image,
1413
};
1514

16-
use ostree_ext::container::ImageReference as OstreeExtImgRef;
15+
use ostree_ext::containers_image_proxy;
1716

1817
use cap_std_ext::cap_std::{ambient_authority, fs::Dir};
1918

2019
use crate::composefs_consts::BOOTC_TAG_PREFIX;
2120
use crate::install::{RootSetup, State};
21+
use crate::lsm;
22+
use crate::podstorage::CStorage;
2223

2324
/// Create a composefs OCI tag name for the given manifest digest.
2425
///
@@ -69,24 +70,30 @@ pub(crate) async fn initialize_composefs_repository(
6970
repo.set_insecure();
7071
}
7172

72-
let OstreeExtImgRef {
73-
name: image_name,
74-
transport,
75-
} = &state.source.imageref;
73+
let imgref = get_imgref(&transport.to_string(), image_name)?;
74+
75+
// On a composefs install, containers-storage lives physically under
76+
// composefs/bootc/storage with a compatibility symlink at
77+
// ostree/bootc -> ../composefs/bootc so the existing /usr/lib/bootc/storage
78+
// symlink (and all runtime code using ostree/bootc/storage) keeps working.
79+
crate::store::ensure_composefs_bootc_link(rootfs_dir)?;
7680

77-
let mut config = crate::deploy::new_proxy_config();
78-
ostree_ext::container::merge_default_container_proxy_opts(&mut config)?;
81+
// Use the unified path: first into containers-storage on the target
82+
// rootfs, then cstor zero-copy into composefs. This ensures the image
83+
// is available for `podman run` from first boot.
84+
let sepolicy = state.load_policy()?;
85+
let run = Dir::open_ambient_dir("/run", ambient_authority())?;
86+
let imgstore = CStorage::create(rootfs_dir, &run, sepolicy.as_ref())?;
87+
let storage_path = root_setup.physical_root_path.join(CStorage::subpath());
7988

80-
// Pull without a reference tag; we tag explicitly afterward so we
81-
// control the tag name format.
8289
let repo = Arc::new(repo);
83-
let (pull_result, _stats) = composefs_oci_pull_image(
84-
&repo,
85-
&format!("{transport}{image_name}"),
86-
None,
87-
Some(config),
88-
)
89-
.await?;
90+
let pull_result =
91+
pull_composefs_unified(&imgstore, storage_path.as_str(), &repo, &imgref).await?;
92+
93+
// SELinux-label the containers-storage now that all pulls are done.
94+
imgstore
95+
.ensure_labeled()
96+
.context("SELinux labeling of containers-storage")?;
9097

9198
// Tag the manifest as a bootc-owned GC root.
9299
let tag = bootc_tag_for_manifest(&pull_result.manifest_digest.to_string());
@@ -107,24 +114,26 @@ pub(crate) async fn initialize_composefs_repository(
107114
Ok(pull_result)
108115
}
109116

110-
/// skopeo (in composefs-rs) doesn't understand "registry:"
111-
/// This function will convert it to "docker://" and return the image ref
117+
/// Convert a transport string and image name into a `containers_image_proxy::ImageReference`.
112118
///
113-
/// Ex
114-
/// docker://quay.io/some-image
115-
/// containers-storage:some-image
116-
/// docker-daemon:some-image-id
117-
pub(crate) fn get_imgref(transport: &str, image: &str) -> String {
118-
let img = image.strip_prefix(":").unwrap_or(&image);
119-
let transport = transport.strip_suffix(":").unwrap_or(&transport);
120-
121-
if transport == "registry" || transport == "docker://" {
122-
format!("docker://{img}")
123-
} else if transport == "docker-daemon" {
124-
format!("docker-daemon:{img}")
125-
} else {
126-
format!("{transport}:{img}")
127-
}
119+
/// The `spec::ImageReference` stores transport as a string (e.g. "registry:",
120+
/// "containers-storage:"). This parses that into a proper typed reference
121+
/// that renders correctly for skopeo (e.g. "docker://quay.io/some-image").
122+
pub(crate) fn get_imgref(
123+
transport: &str,
124+
image: &str,
125+
) -> Result<containers_image_proxy::ImageReference> {
126+
let img = image.strip_prefix(':').unwrap_or(image);
127+
// Normalize: strip trailing separator if present, then parse
128+
// via containers_image_proxy::Transport for proper typed handling.
129+
let transport_str = transport.strip_suffix(':').unwrap_or(transport);
130+
// Build a canonical imgref string so Transport::try_from can parse it.
131+
let imgref_str = format!("{transport_str}:{img}");
132+
let transport: containers_image_proxy::Transport = imgref_str
133+
.as_str()
134+
.try_into()
135+
.with_context(|| format!("Parsing transport from '{imgref_str}'"))?;
136+
Ok(containers_image_proxy::ImageReference::new(transport, img))
128137
}
129138

130139
/// Result of pulling a composefs repository, including the OCI manifest digest
@@ -137,25 +146,89 @@ pub(crate) struct PullRepoResult {
137146
pub(crate) manifest_digest: String,
138147
}
139148

140-
/// Pulls the `image` from `transport` into a composefs repository at /sysroot
141-
/// Checks for boot entries in the image and returns them
149+
/// Pull an image via unified storage: first into bootc-owned containers-storage,
150+
/// then from there into the composefs repository via cstor (zero-copy
151+
/// reflink/hardlink).
152+
///
153+
/// The caller provides:
154+
/// - `imgstore`: the bootc-owned `CStorage` instance (may be on an arbitrary
155+
/// mount point during install, or under `/sysroot` during upgrade)
156+
/// - `storage_path`: the absolute filesystem path to that containers-storage
157+
/// directory, so cstor and skopeo can find it (e.g.
158+
/// `/mnt/sysroot/ostree/bootc/storage` during install, or
159+
/// `/sysroot/ostree/bootc/storage` during upgrade)
160+
///
161+
/// This ensures the image is available in containers-storage for `podman run`
162+
/// while also populating the composefs repo for booting.
163+
async fn pull_composefs_unified(
164+
imgstore: &CStorage,
165+
storage_path: &str,
166+
repo: &Arc<crate::store::ComposefsRepository>,
167+
imgref: &containers_image_proxy::ImageReference,
168+
) -> Result<PullResult<Sha512HashValue>> {
169+
let image = &imgref.name;
170+
171+
// Stage 1: get the image into bootc-owned containers-storage.
172+
if imgref.transport == containers_image_proxy::Transport::ContainerStorage {
173+
// The image is in the default containers-storage (/var/lib/containers/storage).
174+
// Copy it into bootc-owned storage.
175+
tracing::info!("Unified pull: copying {image} from host containers-storage");
176+
imgstore
177+
.pull_from_host_storage(image)
178+
.await
179+
.context("Copying image from host containers-storage into bootc storage")?;
180+
} else {
181+
// For registry (docker://), oci:, docker-daemon:, etc. — pull
182+
// via the native podman API with streaming progress display.
183+
let pull_ref = imgref.to_string();
184+
tracing::info!("Unified pull: fetching {pull_ref} into containers-storage");
185+
imgstore
186+
.pull_with_progress(&pull_ref)
187+
.await
188+
.context("Pulling image into bootc containers-storage")?;
189+
}
190+
191+
// Stage 2: import full OCI structure (layers + config + manifest) from
192+
// containers-storage into composefs via cstor (zero-copy reflink/hardlink).
193+
let cstor_imgref_str = format!("containers-storage:{image}");
194+
tracing::info!("Unified pull: importing from {cstor_imgref_str} (zero-copy)");
195+
196+
let storage = std::path::Path::new(storage_path);
197+
let pull_opts = PullOptions {
198+
additional_image_stores: &[storage],
199+
..Default::default()
200+
};
201+
let pull_result = composefs_oci::pull(repo, &cstor_imgref_str, None, pull_opts)
202+
.await
203+
.context("Importing from containers-storage into composefs")?;
204+
205+
Ok(pull_result)
206+
}
207+
208+
/// Pulls the `image` from `transport` into a composefs repository at /sysroot.
209+
///
210+
/// For registry transports, this uses the unified storage path: the image is
211+
/// first pulled into bootc-owned containers-storage (so it's available for
212+
/// `podman run`), then imported from there into the composefs repo.
213+
///
214+
/// Checks for boot entries in the image and returns them.
142215
#[context("Pulling composefs repository")]
143216
pub(crate) async fn pull_composefs_repo(
144-
transport: &String,
145-
image: &String,
217+
transport: &str,
218+
image: &str,
146219
allow_missing_fsverity: bool,
147220
) -> Result<PullRepoResult> {
148221
const COMPOSEFS_PULL_JOURNAL_ID: &str = "4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8";
149222

223+
let imgref = get_imgref(transport, image)?;
224+
150225
tracing::info!(
151226
message_id = COMPOSEFS_PULL_JOURNAL_ID,
152227
bootc.operation = "pull",
153228
bootc.source_image = image,
154-
bootc.transport = transport,
229+
bootc.transport = %imgref.transport,
155230
bootc.allow_missing_fsverity = allow_missing_fsverity,
156-
"Pulling composefs image {}:{}",
157-
transport,
158-
image
231+
"Pulling composefs image {imgref}",
159232
);
160233

161234
let rootfs_dir = Dir::open_ambient_dir("/sysroot", ambient_authority())?;
@@ -165,17 +238,18 @@ pub(crate) async fn pull_composefs_repo(
165238
repo.set_insecure();
166239
}
167240

168-
let final_imgref = get_imgref(transport, image);
241+
let repo = Arc::new(repo);
169242

170-
tracing::debug!("Image to pull {final_imgref}");
243+
// Create bootc-owned containers-storage on the rootfs.
244+
// Load SELinux policy from the running system so newly pulled layers
245+
// get the correct container_var_lib_t labels.
246+
let root = Dir::open_ambient_dir("/", ambient_authority())?;
247+
let sepolicy = lsm::new_sepolicy_at(&root)?;
248+
let run = Dir::open_ambient_dir("/run", ambient_authority())?;
249+
let imgstore = CStorage::create(&rootfs_dir, &run, sepolicy.as_ref())?;
250+
let storage_path = format!("/sysroot/{}", CStorage::subpath());
171251

172-
let mut config = crate::deploy::new_proxy_config();
173-
ostree_ext::container::merge_default_container_proxy_opts(&mut config)?;
174-
175-
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")?;
252+
let pull_result = pull_composefs_unified(&imgstore, &storage_path, &repo, &imgref).await?;
179253

180254
// Tag the manifest as a bootc-owned GC root.
181255
let tag = bootc_tag_for_manifest(&pull_result.manifest_digest.to_string());
@@ -227,39 +301,41 @@ mod tests {
227301

228302
#[test]
229303
fn test_get_imgref_registry_transport() {
230-
assert_eq!(
231-
get_imgref("registry:", IMAGE_NAME),
232-
format!("docker://{IMAGE_NAME}")
233-
);
304+
let r = get_imgref("registry:", IMAGE_NAME).unwrap();
305+
assert_eq!(r.transport, containers_image_proxy::Transport::Registry);
306+
assert_eq!(r.name, IMAGE_NAME);
307+
assert_eq!(r.to_string(), format!("docker://{IMAGE_NAME}"));
234308
}
235309

236310
#[test]
237311
fn test_get_imgref_containers_storage() {
312+
let r = get_imgref("containers-storage", IMAGE_NAME).unwrap();
238313
assert_eq!(
239-
get_imgref("containers-storage", IMAGE_NAME),
240-
format!("containers-storage:{IMAGE_NAME}")
314+
r.transport,
315+
containers_image_proxy::Transport::ContainerStorage
241316
);
317+
assert_eq!(r.name, IMAGE_NAME);
242318

319+
let r = get_imgref("containers-storage:", IMAGE_NAME).unwrap();
243320
assert_eq!(
244-
get_imgref("containers-storage:", IMAGE_NAME),
245-
format!("containers-storage:{IMAGE_NAME}")
321+
r.transport,
322+
containers_image_proxy::Transport::ContainerStorage
246323
);
324+
assert_eq!(r.name, IMAGE_NAME);
247325
}
248326

249327
#[test]
250328
fn test_get_imgref_edge_cases() {
251-
assert_eq!(
252-
get_imgref("registry", IMAGE_NAME),
253-
format!("docker://{IMAGE_NAME}")
254-
);
329+
let r = get_imgref("registry", IMAGE_NAME).unwrap();
330+
assert_eq!(r.transport, containers_image_proxy::Transport::Registry);
331+
assert_eq!(r.to_string(), format!("docker://{IMAGE_NAME}"));
255332
}
256333

257334
#[test]
258335
fn test_get_imgref_docker_daemon_transport() {
259-
assert_eq!(
260-
get_imgref("docker-daemon", IMAGE_NAME),
261-
format!("docker-daemon:{IMAGE_NAME}")
262-
);
336+
let r = get_imgref("docker-daemon", IMAGE_NAME).unwrap();
337+
assert_eq!(r.transport, containers_image_proxy::Transport::DockerDaemon);
338+
assert_eq!(r.name, IMAGE_NAME);
263339
}
264340

265341
#[test]

0 commit comments

Comments
 (0)