bootc_lib/bootc_composefs/
repo.rs1use fn_error_context::context;
39use std::sync::Arc;
40
41use anyhow::{Context, Result};
42
43use composefs::fsverity::{FsVerityHashValue, Sha512HashValue};
44use composefs::repository::RepositoryConfig;
45use composefs_boot::bootloader::{BootEntry as ComposefsBootEntry, get_boot_resources};
46use composefs_ctl::composefs;
47use composefs_ctl::composefs_boot;
48use composefs_ctl::composefs_oci;
49use composefs_oci::{
50 LocalFetchOpt, PullOptions, PullResult,
51 image::create_filesystem as create_composefs_filesystem, tag_image,
52};
53
54use ostree_ext::containers_image_proxy;
55
56use cap_std_ext::cap_std::{ambient_authority, fs::Dir};
57
58use crate::bootc_composefs::progress;
59use crate::composefs_consts::BOOTC_TAG_PREFIX;
60use crate::install::{RootSetup, State};
61use crate::lsm;
62use crate::podstorage::CStorage;
63use crate::progress_jsonl::ProgressWriter;
64
65pub(crate) fn bootc_tag_for_manifest(manifest_digest: &str) -> String {
71 format!("{BOOTC_TAG_PREFIX}{manifest_digest}")
72}
73
74pub(crate) fn open_composefs_repo(rootfs_dir: &Dir) -> Result<crate::store::ComposefsRepository> {
75 crate::store::ComposefsRepository::open_path(rootfs_dir, "composefs")
76 .context("Failed to open composefs repository")
77}
78
79pub(crate) async fn initialize_composefs_repository(
80 state: &State,
81 root_setup: &RootSetup,
82 allow_missing_fsverity: bool,
83 use_unified: bool,
84) -> Result<PullResult<Sha512HashValue>> {
85 const COMPOSEFS_REPO_INIT_JOURNAL_ID: &str = "5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9";
86
87 let rootfs_dir = &root_setup.physical_root;
88 let image_name = &state.source.imageref.name;
89 let transport = &state.source.imageref.transport;
90
91 tracing::info!(
92 message_id = COMPOSEFS_REPO_INIT_JOURNAL_ID,
93 bootc.operation = "repository_init",
94 bootc.source_image = %image_name,
95 bootc.transport = %transport,
96 bootc.allow_missing_fsverity = allow_missing_fsverity,
97 bootc.unified_storage = use_unified,
98 "Initializing composefs repository for image {}:{}",
99 transport,
100 image_name
101 );
102
103 crate::store::ensure_composefs_dir(rootfs_dir)?;
104
105 let config = RepositoryConfig::new(composefs::fsverity::Algorithm::SHA512);
106 let config = if allow_missing_fsverity {
107 config.set_insecure()
108 } else {
109 config
110 };
111 let (repo, _created) =
112 crate::store::ComposefsRepository::init_path(rootfs_dir, "composefs", config)
113 .context("Failed to initialize composefs repository")?;
114
115 let imgref: containers_image_proxy::ImageReference = state
116 .source
117 .imageref
118 .to_string()
119 .as_str()
120 .try_into()
121 .context("Parsing source image reference")?;
122
123 crate::store::ensure_composefs_bootc_link(rootfs_dir)?;
129
130 let repo = Arc::new(repo);
131
132 let pull_result = if use_unified {
133 let sepolicy = state.load_policy()?;
137 let run = Dir::open_ambient_dir("/run", ambient_authority())?;
138 let imgstore = CStorage::create(rootfs_dir, &run, sepolicy.as_ref())?;
139 let storage_path = root_setup.physical_root_path.join(CStorage::subpath());
140
141 let r = pull_composefs_unified(
144 &imgstore,
145 storage_path.as_str(),
146 &repo,
147 &imgref,
148 false,
149 ProgressWriter::default(),
150 )
151 .await?;
152
153 imgstore
155 .ensure_labeled()
156 .context("SELinux labeling of containers-storage")?;
157 r
158 } else {
159 pull_composefs_direct(&repo, &imgref, false, ProgressWriter::default()).await?
162 };
163
164 let tag = bootc_tag_for_manifest(&pull_result.manifest_digest.to_string());
166 tag_image(&*repo, &pull_result.manifest_digest, &tag)
167 .context("Tagging pulled image as bootc GC root")?;
168
169 tracing::info!(
170 message_id = COMPOSEFS_REPO_INIT_JOURNAL_ID,
171 bootc.operation = "repository_init",
172 bootc.manifest_digest = %pull_result.manifest_digest,
173 bootc.manifest_verity = pull_result.manifest_verity.to_hex(),
174 bootc.config_digest = %pull_result.config_digest,
175 bootc.config_verity = pull_result.config_verity.to_hex(),
176 bootc.tag = tag,
177 "Pulled image into composefs repository",
178 );
179
180 Ok(pull_result)
181}
182
183pub(crate) struct PullRepoResult {
186 pub(crate) repo: crate::store::ComposefsRepository,
187 pub(crate) entries: Vec<ComposefsBootEntry<Sha512HashValue>>,
188 pub(crate) id: Sha512HashValue,
189 pub(crate) manifest_digest: String,
191}
192
193async fn pull_composefs_direct(
199 repo: &Arc<crate::store::ComposefsRepository>,
200 imgref: &containers_image_proxy::ImageReference,
201 quiet: bool,
202 prog: ProgressWriter,
203) -> Result<PullResult<Sha512HashValue>> {
204 let imgref_str = imgref.to_string();
205 tracing::info!("Direct pull: fetching {imgref_str} into composefs repository");
206
207 let mut config = crate::deploy::new_proxy_config();
208 ostree_ext::container::merge_default_container_proxy_opts(&mut config)?;
209
210 let (reporter, prog_task) = progress::spawn(quiet, prog);
211
212 let pull_result = composefs_oci::pull(
213 repo,
214 &imgref_str,
215 None,
216 PullOptions {
217 img_proxy_config: Some(config),
218 progress: Some(reporter),
219 ..Default::default()
220 },
221 )
222 .await;
223
224 prog_task
228 .await
229 .context("Composefs progress task panicked")?;
230
231 pull_result.context("Pulling image into composefs repository")
232}
233
234async fn pull_composefs_unified(
249 imgstore: &CStorage,
250 storage_path: &str,
251 repo: &Arc<crate::store::ComposefsRepository>,
252 imgref: &containers_image_proxy::ImageReference,
253 quiet: bool,
254 prog: ProgressWriter,
255) -> Result<PullResult<Sha512HashValue>> {
256 let image = &imgref.name;
257
258 if imgref.transport == containers_image_proxy::Transport::ContainerStorage {
260 tracing::info!("Unified pull: copying {image} from host containers-storage");
263 imgstore
264 .pull_from_host_storage(image)
265 .await
266 .context("Copying image from host containers-storage into bootc storage")?;
267 } else {
268 let pull_ref = imgref.to_string();
271 tracing::info!("Unified pull: fetching {pull_ref} into containers-storage");
272 imgstore
273 .pull_with_progress(&pull_ref)
274 .await
275 .context("Pulling image into bootc containers-storage")?;
276 }
277
278 let cstor_imgref_str = format!("containers-storage:{image}");
281 tracing::info!("Unified pull: importing from {cstor_imgref_str} (zero-copy)");
282
283 let storage = std::path::Path::new(storage_path);
284 let (reporter, prog_task) = progress::spawn(quiet, prog);
285 let pull_opts = PullOptions {
286 local_fetch: LocalFetchOpt::ZeroCopy,
292 storage_root: Some(storage),
293 progress: Some(reporter),
294 ..Default::default()
295 };
296 let pull_result = composefs_oci::pull(repo, &cstor_imgref_str, None, pull_opts).await;
297
298 prog_task
299 .await
300 .context("Composefs progress task panicked")?;
301
302 let pull_result = pull_result.context("Importing from containers-storage into composefs")?;
303
304 Ok(pull_result)
305}
306
307#[context("Pulling composefs repository")]
318pub(crate) async fn pull_composefs_repo(
319 spec_imgref: &crate::spec::ImageReference,
320 allow_missing_fsverity: bool,
321 use_unified: bool,
322 quiet: bool,
323 prog: ProgressWriter,
324) -> Result<PullRepoResult> {
325 const COMPOSEFS_PULL_JOURNAL_ID: &str = "4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8";
326
327 let imgref = spec_imgref.to_image_proxy_ref()?;
328
329 tracing::info!(
330 message_id = COMPOSEFS_PULL_JOURNAL_ID,
331 bootc.operation = "pull",
332 bootc.source_image = &spec_imgref.image,
333 bootc.transport = %imgref.transport,
334 bootc.allow_missing_fsverity = allow_missing_fsverity,
335 bootc.unified_storage = use_unified,
336 "Pulling composefs image {imgref}",
337 );
338
339 let rootfs_dir = Dir::open_ambient_dir("/sysroot", ambient_authority())?;
340
341 let mut repo = open_composefs_repo(&rootfs_dir).context("Opening composefs repo")?;
342 if allow_missing_fsverity {
343 repo.set_insecure();
344 }
345
346 let repo = Arc::new(repo);
347
348 let upgrade_result =
356 composefs_oci::upgrade_repo(&repo).context("Upgrading old-format OCI images")?;
357 if upgrade_result.upgraded > 0 {
358 tracing::info!(
359 "Upgraded {} old-format OCI image(s) to current format",
360 upgrade_result.upgraded
361 );
362 }
363
364 let pull_result = if use_unified {
365 let root = Dir::open_ambient_dir("/", ambient_authority())?;
369 let sepolicy = lsm::new_sepolicy_at(&root)?;
370 let run = Dir::open_ambient_dir("/run", ambient_authority())?;
371 let imgstore = CStorage::create(&rootfs_dir, &run, sepolicy.as_ref())?;
372 let storage_path = format!("/sysroot/{}", CStorage::subpath());
373
374 pull_composefs_unified(&imgstore, &storage_path, &repo, &imgref, quiet, prog).await?
375 } else {
376 pull_composefs_direct(&repo, &imgref, quiet, prog).await?
377 };
378
379 let tag = bootc_tag_for_manifest(&pull_result.manifest_digest.to_string());
381 tag_image(&*repo, &pull_result.manifest_digest, &tag)
382 .context("Tagging pulled image as bootc GC root")?;
383
384 tracing::info!(
385 message_id = COMPOSEFS_PULL_JOURNAL_ID,
386 bootc.operation = "pull",
387 bootc.manifest_digest = %pull_result.manifest_digest,
388 bootc.manifest_verity = pull_result.manifest_verity.to_hex(),
389 bootc.config_digest = %pull_result.config_digest,
390 bootc.config_verity = pull_result.config_verity.to_hex(),
391 bootc.tag = tag,
392 "Pulled image into composefs repository",
393 );
394
395 let id = composefs_oci::generate_boot_image(&repo, &pull_result.manifest_digest)
397 .context("Generating bootable EROFS image")?;
398
399 let fs = create_composefs_filesystem(&*repo, &pull_result.config_digest, None)
401 .context("Creating composefs filesystem for boot entry discovery")?;
402 let entries =
403 get_boot_resources(&fs, &*repo).context("Extracting boot entries from OCI image")?;
404
405 let mut repo = Arc::try_unwrap(repo).map_err(|_| {
407 anyhow::anyhow!("BUG: Arc<Repository> still has other references after pull completed")
408 })?;
409 if allow_missing_fsverity {
410 repo.set_insecure();
411 }
412
413 Ok(PullRepoResult {
414 repo,
415 entries,
416 id,
417 manifest_digest: pull_result.manifest_digest.to_string(),
418 })
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424
425 #[test]
426 fn test_bootc_tag_for_manifest() {
427 let digest = "sha256:abc123def456";
428 let tag = bootc_tag_for_manifest(digest);
429 assert_eq!(tag, "localhost/bootc-sha256:abc123def456");
430 assert!(tag.starts_with(BOOTC_TAG_PREFIX));
431 }
432}