bootc_lib/bootc_composefs/
repo.rs1use fn_error_context::context;
2use std::sync::Arc;
3
4use anyhow::{Context, Result};
5
6use composefs::fsverity::{FsVerityHashValue, Sha512HashValue};
7use composefs_boot::bootloader::{BootEntry as ComposefsBootEntry, get_boot_resources};
8use composefs_ctl::composefs;
9use composefs_ctl::composefs_boot;
10use composefs_ctl::composefs_oci;
11use composefs_oci::{
12 LocalFetchOpt, PullOptions, PullResult,
13 image::create_filesystem as create_composefs_filesystem, tag_image,
14};
15
16use ostree_ext::containers_image_proxy;
17
18use cap_std_ext::cap_std::{ambient_authority, fs::Dir};
19
20use crate::composefs_consts::BOOTC_TAG_PREFIX;
21use crate::install::{RootSetup, State};
22use crate::lsm;
23use crate::podstorage::CStorage;
24
25pub(crate) fn bootc_tag_for_manifest(manifest_digest: &str) -> String {
31 format!("{BOOTC_TAG_PREFIX}{manifest_digest}")
32}
33
34pub(crate) fn open_composefs_repo(rootfs_dir: &Dir) -> Result<crate::store::ComposefsRepository> {
35 crate::store::ComposefsRepository::open_path(rootfs_dir, "composefs")
36 .context("Failed to open composefs repository")
37}
38
39pub(crate) async fn initialize_composefs_repository(
40 state: &State,
41 root_setup: &RootSetup,
42 allow_missing_fsverity: bool,
43 use_unified: bool,
44) -> Result<PullResult<Sha512HashValue>> {
45 const COMPOSEFS_REPO_INIT_JOURNAL_ID: &str = "5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9";
46
47 let rootfs_dir = &root_setup.physical_root;
48 let image_name = &state.source.imageref.name;
49 let transport = &state.source.imageref.transport;
50
51 tracing::info!(
52 message_id = COMPOSEFS_REPO_INIT_JOURNAL_ID,
53 bootc.operation = "repository_init",
54 bootc.source_image = %image_name,
55 bootc.transport = %transport,
56 bootc.allow_missing_fsverity = allow_missing_fsverity,
57 bootc.unified_storage = use_unified,
58 "Initializing composefs repository for image {}:{}",
59 transport,
60 image_name
61 );
62
63 crate::store::ensure_composefs_dir(rootfs_dir)?;
64
65 let (mut repo, _created) = crate::store::ComposefsRepository::init_path(
66 rootfs_dir,
67 "composefs",
68 composefs::fsverity::Algorithm::SHA512,
69 !allow_missing_fsverity,
70 )
71 .context("Failed to initialize composefs repository")?;
72 if allow_missing_fsverity {
73 repo.set_insecure();
74 }
75
76 let imgref: containers_image_proxy::ImageReference = state
77 .source
78 .imageref
79 .to_string()
80 .as_str()
81 .try_into()
82 .context("Parsing source image reference")?;
83
84 crate::store::ensure_composefs_bootc_link(rootfs_dir)?;
90
91 let repo = Arc::new(repo);
92
93 let pull_result = if use_unified {
94 let sepolicy = state.load_policy()?;
98 let run = Dir::open_ambient_dir("/run", ambient_authority())?;
99 let imgstore = CStorage::create(rootfs_dir, &run, sepolicy.as_ref())?;
100 let storage_path = root_setup.physical_root_path.join(CStorage::subpath());
101
102 let r = pull_composefs_unified(&imgstore, storage_path.as_str(), &repo, &imgref).await?;
103
104 imgstore
106 .ensure_labeled()
107 .context("SELinux labeling of containers-storage")?;
108 r
109 } else {
110 pull_composefs_direct(&repo, &imgref).await?
113 };
114
115 let tag = bootc_tag_for_manifest(&pull_result.manifest_digest.to_string());
117 tag_image(&*repo, &pull_result.manifest_digest, &tag)
118 .context("Tagging pulled image as bootc GC root")?;
119
120 tracing::info!(
121 message_id = COMPOSEFS_REPO_INIT_JOURNAL_ID,
122 bootc.operation = "repository_init",
123 bootc.manifest_digest = %pull_result.manifest_digest,
124 bootc.manifest_verity = pull_result.manifest_verity.to_hex(),
125 bootc.config_digest = %pull_result.config_digest,
126 bootc.config_verity = pull_result.config_verity.to_hex(),
127 bootc.tag = tag,
128 "Pulled image into composefs repository",
129 );
130
131 Ok(pull_result)
132}
133
134pub(crate) struct PullRepoResult {
137 pub(crate) repo: crate::store::ComposefsRepository,
138 pub(crate) entries: Vec<ComposefsBootEntry<Sha512HashValue>>,
139 pub(crate) id: Sha512HashValue,
140 pub(crate) manifest_digest: String,
142}
143
144async fn pull_composefs_direct(
150 repo: &Arc<crate::store::ComposefsRepository>,
151 imgref: &containers_image_proxy::ImageReference,
152) -> Result<PullResult<Sha512HashValue>> {
153 let imgref_str = imgref.to_string();
154 tracing::info!("Direct pull: fetching {imgref_str} into composefs repository");
155
156 let pull_result = composefs_oci::pull(repo, &imgref_str, None, PullOptions::default())
157 .await
158 .context("Pulling image into composefs repository")?;
159
160 Ok(pull_result)
161}
162
163async fn pull_composefs_unified(
178 imgstore: &CStorage,
179 storage_path: &str,
180 repo: &Arc<crate::store::ComposefsRepository>,
181 imgref: &containers_image_proxy::ImageReference,
182) -> Result<PullResult<Sha512HashValue>> {
183 let image = &imgref.name;
184
185 if imgref.transport == containers_image_proxy::Transport::ContainerStorage {
187 tracing::info!("Unified pull: copying {image} from host containers-storage");
190 imgstore
191 .pull_from_host_storage(image)
192 .await
193 .context("Copying image from host containers-storage into bootc storage")?;
194 } else {
195 let pull_ref = imgref.to_string();
198 tracing::info!("Unified pull: fetching {pull_ref} into containers-storage");
199 imgstore
200 .pull_with_progress(&pull_ref)
201 .await
202 .context("Pulling image into bootc containers-storage")?;
203 }
204
205 let cstor_imgref_str = format!("containers-storage:{image}");
208 tracing::info!("Unified pull: importing from {cstor_imgref_str} (zero-copy)");
209
210 let storage = std::path::Path::new(storage_path);
211 let pull_opts = PullOptions {
212 local_fetch: LocalFetchOpt::ZeroCopy,
218 storage_root: Some(storage),
219 ..Default::default()
220 };
221 let pull_result = composefs_oci::pull(repo, &cstor_imgref_str, None, pull_opts)
222 .await
223 .context("Importing from containers-storage into composefs")?;
224
225 Ok(pull_result)
226}
227
228#[context("Pulling composefs repository")]
239pub(crate) async fn pull_composefs_repo(
240 spec_imgref: &crate::spec::ImageReference,
241 allow_missing_fsverity: bool,
242 use_unified: bool,
243) -> Result<PullRepoResult> {
244 const COMPOSEFS_PULL_JOURNAL_ID: &str = "4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8";
245
246 let imgref = spec_imgref.to_image_proxy_ref()?;
247
248 tracing::info!(
249 message_id = COMPOSEFS_PULL_JOURNAL_ID,
250 bootc.operation = "pull",
251 bootc.source_image = &spec_imgref.image,
252 bootc.transport = %imgref.transport,
253 bootc.allow_missing_fsverity = allow_missing_fsverity,
254 bootc.unified_storage = use_unified,
255 "Pulling composefs image {imgref}",
256 );
257
258 let rootfs_dir = Dir::open_ambient_dir("/sysroot", ambient_authority())?;
259
260 let mut repo = open_composefs_repo(&rootfs_dir).context("Opening composefs repo")?;
261 if allow_missing_fsverity {
262 repo.set_insecure();
263 }
264
265 let repo = Arc::new(repo);
266
267 let upgrade_result =
275 composefs_oci::upgrade_repo(&repo).context("Upgrading old-format OCI images")?;
276 if upgrade_result.upgraded > 0 {
277 tracing::info!(
278 "Upgraded {} old-format OCI image(s) to current format",
279 upgrade_result.upgraded
280 );
281 }
282
283 let pull_result = if use_unified {
284 let root = Dir::open_ambient_dir("/", ambient_authority())?;
288 let sepolicy = lsm::new_sepolicy_at(&root)?;
289 let run = Dir::open_ambient_dir("/run", ambient_authority())?;
290 let imgstore = CStorage::create(&rootfs_dir, &run, sepolicy.as_ref())?;
291 let storage_path = format!("/sysroot/{}", CStorage::subpath());
292
293 pull_composefs_unified(&imgstore, &storage_path, &repo, &imgref).await?
294 } else {
295 pull_composefs_direct(&repo, &imgref).await?
296 };
297
298 let tag = bootc_tag_for_manifest(&pull_result.manifest_digest.to_string());
300 tag_image(&*repo, &pull_result.manifest_digest, &tag)
301 .context("Tagging pulled image as bootc GC root")?;
302
303 tracing::info!(
304 message_id = COMPOSEFS_PULL_JOURNAL_ID,
305 bootc.operation = "pull",
306 bootc.manifest_digest = %pull_result.manifest_digest,
307 bootc.manifest_verity = pull_result.manifest_verity.to_hex(),
308 bootc.config_digest = %pull_result.config_digest,
309 bootc.config_verity = pull_result.config_verity.to_hex(),
310 bootc.tag = tag,
311 "Pulled image into composefs repository",
312 );
313
314 let id = composefs_oci::generate_boot_image(&repo, &pull_result.manifest_digest)
316 .context("Generating bootable EROFS image")?;
317
318 let fs = create_composefs_filesystem(&*repo, &pull_result.config_digest, None)
320 .context("Creating composefs filesystem for boot entry discovery")?;
321 let entries =
322 get_boot_resources(&fs, &*repo).context("Extracting boot entries from OCI image")?;
323
324 let mut repo = Arc::try_unwrap(repo).map_err(|_| {
326 anyhow::anyhow!("BUG: Arc<Repository> still has other references after pull completed")
327 })?;
328 if allow_missing_fsverity {
329 repo.set_insecure();
330 }
331
332 Ok(PullRepoResult {
333 repo,
334 entries,
335 id,
336 manifest_digest: pull_result.manifest_digest.to_string(),
337 })
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343
344 #[test]
345 fn test_bootc_tag_for_manifest() {
346 let digest = "sha256:abc123def456";
347 let tag = bootc_tag_for_manifest(digest);
348 assert_eq!(tag, "localhost/bootc-sha256:abc123def456");
349 assert!(tag.starts_with(BOOTC_TAG_PREFIX));
350 }
351}