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::composefs_consts::BOOTC_TAG_PREFIX;
59use crate::install::{RootSetup, State};
60use crate::lsm;
61use crate::podstorage::CStorage;
62
63pub(crate) fn bootc_tag_for_manifest(manifest_digest: &str) -> String {
69 format!("{BOOTC_TAG_PREFIX}{manifest_digest}")
70}
71
72pub(crate) fn open_composefs_repo(rootfs_dir: &Dir) -> Result<crate::store::ComposefsRepository> {
73 crate::store::ComposefsRepository::open_path(rootfs_dir, "composefs")
74 .context("Failed to open composefs repository")
75}
76
77pub(crate) async fn initialize_composefs_repository(
78 state: &State,
79 root_setup: &RootSetup,
80 allow_missing_fsverity: bool,
81 use_unified: bool,
82) -> Result<PullResult<Sha512HashValue>> {
83 const COMPOSEFS_REPO_INIT_JOURNAL_ID: &str = "5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9";
84
85 let rootfs_dir = &root_setup.physical_root;
86 let image_name = &state.source.imageref.name;
87 let transport = &state.source.imageref.transport;
88
89 tracing::info!(
90 message_id = COMPOSEFS_REPO_INIT_JOURNAL_ID,
91 bootc.operation = "repository_init",
92 bootc.source_image = %image_name,
93 bootc.transport = %transport,
94 bootc.allow_missing_fsverity = allow_missing_fsverity,
95 bootc.unified_storage = use_unified,
96 "Initializing composefs repository for image {}:{}",
97 transport,
98 image_name
99 );
100
101 crate::store::ensure_composefs_dir(rootfs_dir)?;
102
103 let config = RepositoryConfig::new(composefs::fsverity::Algorithm::SHA512);
104 let config = if allow_missing_fsverity {
105 config.set_insecure()
106 } else {
107 config
108 };
109 let (repo, _created) =
110 crate::store::ComposefsRepository::init_path(rootfs_dir, "composefs", config)
111 .context("Failed to initialize composefs repository")?;
112
113 let imgref: containers_image_proxy::ImageReference = state
114 .source
115 .imageref
116 .to_string()
117 .as_str()
118 .try_into()
119 .context("Parsing source image reference")?;
120
121 crate::store::ensure_composefs_bootc_link(rootfs_dir)?;
127
128 let repo = Arc::new(repo);
129
130 let pull_result = if use_unified {
131 let sepolicy = state.load_policy()?;
135 let run = Dir::open_ambient_dir("/run", ambient_authority())?;
136 let imgstore = CStorage::create(rootfs_dir, &run, sepolicy.as_ref())?;
137 let storage_path = root_setup.physical_root_path.join(CStorage::subpath());
138
139 let r = pull_composefs_unified(&imgstore, storage_path.as_str(), &repo, &imgref).await?;
140
141 imgstore
143 .ensure_labeled()
144 .context("SELinux labeling of containers-storage")?;
145 r
146 } else {
147 pull_composefs_direct(&repo, &imgref).await?
150 };
151
152 let tag = bootc_tag_for_manifest(&pull_result.manifest_digest.to_string());
154 tag_image(&*repo, &pull_result.manifest_digest, &tag)
155 .context("Tagging pulled image as bootc GC root")?;
156
157 tracing::info!(
158 message_id = COMPOSEFS_REPO_INIT_JOURNAL_ID,
159 bootc.operation = "repository_init",
160 bootc.manifest_digest = %pull_result.manifest_digest,
161 bootc.manifest_verity = pull_result.manifest_verity.to_hex(),
162 bootc.config_digest = %pull_result.config_digest,
163 bootc.config_verity = pull_result.config_verity.to_hex(),
164 bootc.tag = tag,
165 "Pulled image into composefs repository",
166 );
167
168 Ok(pull_result)
169}
170
171pub(crate) struct PullRepoResult {
174 pub(crate) repo: crate::store::ComposefsRepository,
175 pub(crate) entries: Vec<ComposefsBootEntry<Sha512HashValue>>,
176 pub(crate) id: Sha512HashValue,
177 pub(crate) manifest_digest: String,
179}
180
181async fn pull_composefs_direct(
187 repo: &Arc<crate::store::ComposefsRepository>,
188 imgref: &containers_image_proxy::ImageReference,
189) -> Result<PullResult<Sha512HashValue>> {
190 let imgref_str = imgref.to_string();
191 tracing::info!("Direct pull: fetching {imgref_str} into composefs repository");
192
193 let mut config = crate::deploy::new_proxy_config();
194 ostree_ext::container::apply_container_proxy_opts_for_transport(&mut config, imgref.transport)?;
195
196 let pull_result = composefs_oci::pull(
197 repo,
198 &imgref_str,
199 None,
200 PullOptions {
201 img_proxy_config: Some(config),
202 ..Default::default()
203 },
204 )
205 .await
206 .context("Pulling image into composefs repository")?;
207
208 Ok(pull_result)
209}
210
211async fn pull_composefs_unified(
226 imgstore: &CStorage,
227 storage_path: &str,
228 repo: &Arc<crate::store::ComposefsRepository>,
229 imgref: &containers_image_proxy::ImageReference,
230) -> Result<PullResult<Sha512HashValue>> {
231 let image = &imgref.name;
232
233 if imgref.transport == containers_image_proxy::Transport::ContainerStorage {
235 tracing::info!("Unified pull: copying {image} from host containers-storage");
238 imgstore
239 .pull_from_host_storage(image)
240 .await
241 .context("Copying image from host containers-storage into bootc storage")?;
242 } else {
243 let pull_ref = imgref.to_string();
246 tracing::info!("Unified pull: fetching {pull_ref} into containers-storage");
247 imgstore
248 .pull_with_progress(&pull_ref)
249 .await
250 .context("Pulling image into bootc containers-storage")?;
251 }
252
253 let cstor_imgref_str = format!("containers-storage:{image}");
256 tracing::info!("Unified pull: importing from {cstor_imgref_str} (zero-copy)");
257
258 let storage = std::path::Path::new(storage_path);
259 let pull_opts = PullOptions {
260 local_fetch: LocalFetchOpt::ZeroCopy,
266 storage_root: Some(storage),
267 ..Default::default()
268 };
269 let pull_result = composefs_oci::pull(repo, &cstor_imgref_str, None, pull_opts)
270 .await
271 .context("Importing from containers-storage into composefs")?;
272
273 Ok(pull_result)
274}
275
276#[context("Pulling composefs repository")]
287pub(crate) async fn pull_composefs_repo(
288 spec_imgref: &crate::spec::ImageReference,
289 allow_missing_fsverity: bool,
290 use_unified: bool,
291) -> Result<PullRepoResult> {
292 const COMPOSEFS_PULL_JOURNAL_ID: &str = "4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8";
293
294 let imgref = spec_imgref.to_image_proxy_ref()?;
295
296 tracing::info!(
297 message_id = COMPOSEFS_PULL_JOURNAL_ID,
298 bootc.operation = "pull",
299 bootc.source_image = &spec_imgref.image,
300 bootc.transport = %imgref.transport,
301 bootc.allow_missing_fsverity = allow_missing_fsverity,
302 bootc.unified_storage = use_unified,
303 "Pulling composefs image {imgref}",
304 );
305
306 let rootfs_dir = Dir::open_ambient_dir("/sysroot", ambient_authority())?;
307
308 let mut repo = open_composefs_repo(&rootfs_dir).context("Opening composefs repo")?;
309 if allow_missing_fsverity {
310 repo.set_insecure();
311 }
312
313 let repo = Arc::new(repo);
314
315 let upgrade_result =
323 composefs_oci::upgrade_repo(&repo).context("Upgrading old-format OCI images")?;
324 if upgrade_result.upgraded > 0 {
325 tracing::info!(
326 "Upgraded {} old-format OCI image(s) to current format",
327 upgrade_result.upgraded
328 );
329 }
330
331 let pull_result = if use_unified {
332 let root = Dir::open_ambient_dir("/", ambient_authority())?;
336 let sepolicy = lsm::new_sepolicy_at(&root)?;
337 let run = Dir::open_ambient_dir("/run", ambient_authority())?;
338 let imgstore = CStorage::create(&rootfs_dir, &run, sepolicy.as_ref())?;
339 let storage_path = format!("/sysroot/{}", CStorage::subpath());
340
341 pull_composefs_unified(&imgstore, &storage_path, &repo, &imgref).await?
342 } else {
343 pull_composefs_direct(&repo, &imgref).await?
344 };
345
346 let tag = bootc_tag_for_manifest(&pull_result.manifest_digest.to_string());
348 tag_image(&*repo, &pull_result.manifest_digest, &tag)
349 .context("Tagging pulled image as bootc GC root")?;
350
351 tracing::info!(
352 message_id = COMPOSEFS_PULL_JOURNAL_ID,
353 bootc.operation = "pull",
354 bootc.manifest_digest = %pull_result.manifest_digest,
355 bootc.manifest_verity = pull_result.manifest_verity.to_hex(),
356 bootc.config_digest = %pull_result.config_digest,
357 bootc.config_verity = pull_result.config_verity.to_hex(),
358 bootc.tag = tag,
359 "Pulled image into composefs repository",
360 );
361
362 let id = composefs_oci::generate_boot_image(&repo, &pull_result.manifest_digest)
364 .context("Generating bootable EROFS image")?;
365
366 let fs = create_composefs_filesystem(&*repo, &pull_result.config_digest, None)
368 .context("Creating composefs filesystem for boot entry discovery")?;
369 let entries =
370 get_boot_resources(&fs, &*repo).context("Extracting boot entries from OCI image")?;
371
372 let mut repo = Arc::try_unwrap(repo).map_err(|_| {
374 anyhow::anyhow!("BUG: Arc<Repository> still has other references after pull completed")
375 })?;
376 if allow_missing_fsverity {
377 repo.set_insecure();
378 }
379
380 Ok(PullRepoResult {
381 repo,
382 entries,
383 id,
384 manifest_digest: pull_result.manifest_digest.to_string(),
385 })
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391
392 #[test]
393 fn test_bootc_tag_for_manifest() {
394 let digest = "sha256:abc123def456";
395 let tag = bootc_tag_for_manifest(digest);
396 assert_eq!(tag, "localhost/bootc-sha256:abc123def456");
397 assert!(tag.starts_with(BOOTC_TAG_PREFIX));
398 }
399}