|
4 | 4 |
|
5 | 5 | use std::collections::HashSet; |
6 | 6 | use std::io::{BufRead, Write}; |
| 7 | +use std::process::Command; |
7 | 8 |
|
8 | | -use anyhow::Ok; |
9 | | -use anyhow::{anyhow, Context, Result}; |
| 9 | +use anyhow::{Context, Result, anyhow}; |
10 | 10 | use bootc_kernel_cmdline::utf8::CmdlineOwned; |
11 | 11 | use cap_std::fs::{Dir, MetadataExt}; |
12 | 12 | use cap_std_ext::cap_std; |
@@ -93,6 +93,17 @@ pub(crate) async fn new_importer( |
93 | 93 | Ok(imp) |
94 | 94 | } |
95 | 95 |
|
| 96 | +/// Wrapper for pulling a container image with a custom proxy config (e.g. for unified storage). |
| 97 | +pub(crate) async fn new_importer_with_config( |
| 98 | + repo: &ostree::Repo, |
| 99 | + imgref: &ostree_container::OstreeImageReference, |
| 100 | + config: ostree_ext::containers_image_proxy::ImageProxyConfig, |
| 101 | +) -> Result<ostree_container::store::ImageImporter> { |
| 102 | + let mut imp = ostree_container::store::ImageImporter::new(repo, imgref, config).await?; |
| 103 | + imp.require_bootable(); |
| 104 | + Ok(imp) |
| 105 | +} |
| 106 | + |
96 | 107 | pub(crate) fn check_bootc_label(config: &ostree_ext::oci_spec::image::ImageConfiguration) { |
97 | 108 | if let Some(label) = |
98 | 109 | labels_of_config(config).and_then(|labels| labels.get(crate::metadata::BOOTC_COMPAT_LABEL)) |
@@ -316,6 +327,18 @@ pub(crate) async fn prune_container_store(sysroot: &Storage) -> Result<()> { |
316 | 327 | for deployment in deployments { |
317 | 328 | let bound = crate::boundimage::query_bound_images_for_deployment(ostree, &deployment)?; |
318 | 329 | all_bound_images.extend(bound.into_iter()); |
| 330 | + // Also include the host image itself |
| 331 | + // Note: Use just the image name (not the full transport:image format) because |
| 332 | + // podman's image names don't include the transport prefix. |
| 333 | + if let Some(host_image) = crate::status::boot_entry_from_deployment(ostree, &deployment)? |
| 334 | + .image |
| 335 | + .map(|i| i.image) |
| 336 | + { |
| 337 | + all_bound_images.push(crate::boundimage::BoundImage { |
| 338 | + image: host_image.image.clone(), |
| 339 | + auth_file: None, |
| 340 | + }); |
| 341 | + } |
319 | 342 | } |
320 | 343 | // Convert to a hashset of just the image names |
321 | 344 | let image_names = HashSet::from_iter(all_bound_images.iter().map(|img| img.image.as_str())); |
@@ -381,6 +404,180 @@ pub(crate) async fn prepare_for_pull( |
381 | 404 | Ok(PreparedPullResult::Ready(Box::new(prepared_image))) |
382 | 405 | } |
383 | 406 |
|
| 407 | +/// Check whether the image exists in bootc's unified container storage. |
| 408 | +/// |
| 409 | +/// This is used for auto-detection: if the image already exists in bootc storage |
| 410 | +/// (e.g., from a previous `bootc image set-unified` or LBI pull), we can use |
| 411 | +/// the unified storage path for faster imports. |
| 412 | +/// |
| 413 | +/// Returns true if the image exists in bootc storage. |
| 414 | +pub(crate) async fn image_exists_in_unified_storage( |
| 415 | + store: &Storage, |
| 416 | + imgref: &ImageReference, |
| 417 | +) -> bool { |
| 418 | + let imgstore = match store.get_ensure_imgstore() { |
| 419 | + Ok(s) => s, |
| 420 | + Err(e) => { |
| 421 | + tracing::warn!("Failed to access bootc storage: {e}; falling back to standard pull"); |
| 422 | + return false; |
| 423 | + } |
| 424 | + }; |
| 425 | + |
| 426 | + let image_ref_str = imgref.to_transport_image(); |
| 427 | + match imgstore.exists(&image_ref_str).await { |
| 428 | + Ok(v) => v, |
| 429 | + Err(e) => { |
| 430 | + tracing::warn!( |
| 431 | + "Failed to check bootc storage for image: {e}; falling back to standard pull" |
| 432 | + ); |
| 433 | + false |
| 434 | + } |
| 435 | + } |
| 436 | +} |
| 437 | + |
| 438 | +/// Unified approach: Use bootc's CStorage to pull the image, then prepare from containers-storage. |
| 439 | +/// This reuses the same infrastructure as LBIs. |
| 440 | +/// |
| 441 | +/// The `sysroot_path` parameter specifies the path to the sysroot where bootc storage is located. |
| 442 | +/// During install, this should be the path to the target disk's mount point. |
| 443 | +/// During upgrade/switch on a running system, pass `None` to use the default `/sysroot`. |
| 444 | +pub(crate) async fn prepare_for_pull_unified( |
| 445 | + repo: &ostree::Repo, |
| 446 | + imgref: &ImageReference, |
| 447 | + target_imgref: Option<&OstreeImageReference>, |
| 448 | + store: &Storage, |
| 449 | + sysroot_path: Option<&camino::Utf8Path>, |
| 450 | +) -> Result<PreparedPullResult> { |
| 451 | + // Get or initialize the bootc container storage (same as used for LBIs) |
| 452 | + let imgstore = store.get_ensure_imgstore()?; |
| 453 | + |
| 454 | + let image_ref_str = imgref.to_transport_image(); |
| 455 | + |
| 456 | + // Always pull to ensure we have the latest image, whether from a remote |
| 457 | + // registry or a locally rebuilt image |
| 458 | + tracing::info!( |
| 459 | + "Unified pull: pulling from transport '{}' to bootc storage", |
| 460 | + &imgref.transport |
| 461 | + ); |
| 462 | + |
| 463 | + // Pull the image to bootc storage using the same method as LBIs |
| 464 | + // Show a spinner since podman pull can take a while and doesn't output progress |
| 465 | + let pull_msg = format!("Pulling {} to bootc storage", &image_ref_str); |
| 466 | + async_task_with_spinner(&pull_msg, async move { |
| 467 | + imgstore |
| 468 | + .pull(&image_ref_str, crate::podstorage::PullMode::Always) |
| 469 | + .await |
| 470 | + }) |
| 471 | + .await?; |
| 472 | + |
| 473 | + // Now create a containers-storage reference to read from bootc storage |
| 474 | + tracing::info!("Unified pull: now importing from containers-storage transport"); |
| 475 | + let containers_storage_imgref = ImageReference { |
| 476 | + transport: "containers-storage".to_string(), |
| 477 | + image: imgref.image.clone(), |
| 478 | + signature: imgref.signature.clone(), |
| 479 | + }; |
| 480 | + let ostree_imgref = OstreeImageReference::from(containers_storage_imgref); |
| 481 | + |
| 482 | + // Configure the importer to use bootc storage as an additional image store |
| 483 | + let mut config = ostree_ext::containers_image_proxy::ImageProxyConfig::default(); |
| 484 | + let mut cmd = Command::new("skopeo"); |
| 485 | + // Use the actual physical path to bootc storage |
| 486 | + // During install, this is the target disk's mount point; otherwise default to /sysroot |
| 487 | + let sysroot_base = sysroot_path |
| 488 | + .map(|p| p.to_string()) |
| 489 | + .unwrap_or_else(|| "/sysroot".to_string()); |
| 490 | + let storage_path = format!( |
| 491 | + "{}/{}", |
| 492 | + sysroot_base, |
| 493 | + crate::podstorage::CStorage::subpath() |
| 494 | + ); |
| 495 | + crate::podstorage::set_additional_image_store(&mut cmd, &storage_path); |
| 496 | + config.skopeo_cmd = Some(cmd); |
| 497 | + |
| 498 | + // Use the preparation flow with the custom config |
| 499 | + let mut imp = new_importer_with_config(repo, &ostree_imgref, config).await?; |
| 500 | + if let Some(target) = target_imgref { |
| 501 | + imp.set_target(target); |
| 502 | + } |
| 503 | + let prep = match imp.prepare().await? { |
| 504 | + PrepareResult::AlreadyPresent(c) => { |
| 505 | + println!("No changes in {imgref:#} => {}", c.manifest_digest); |
| 506 | + return Ok(PreparedPullResult::AlreadyPresent(Box::new((*c).into()))); |
| 507 | + } |
| 508 | + PrepareResult::Ready(p) => p, |
| 509 | + }; |
| 510 | + check_bootc_label(&prep.config); |
| 511 | + if let Some(warning) = prep.deprecated_warning() { |
| 512 | + ostree_ext::cli::print_deprecated_warning(warning).await; |
| 513 | + } |
| 514 | + ostree_ext::cli::print_layer_status(&prep); |
| 515 | + let layers_to_fetch = prep.layers_to_fetch().collect::<Result<Vec<_>>>()?; |
| 516 | + |
| 517 | + // Log that we're importing a new image from containers-storage |
| 518 | + const PULLING_NEW_IMAGE_ID: &str = "6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0"; |
| 519 | + tracing::info!( |
| 520 | + message_id = PULLING_NEW_IMAGE_ID, |
| 521 | + bootc.image.reference = &imgref.image, |
| 522 | + bootc.image.transport = "containers-storage", |
| 523 | + bootc.original_transport = &imgref.transport, |
| 524 | + bootc.status = "importing_from_storage", |
| 525 | + "Importing image from bootc storage: {}", |
| 526 | + ostree_imgref |
| 527 | + ); |
| 528 | + |
| 529 | + let prepared_image = PreparedImportMeta { |
| 530 | + imp, |
| 531 | + n_layers_to_fetch: layers_to_fetch.len(), |
| 532 | + layers_total: prep.all_layers().count(), |
| 533 | + bytes_to_fetch: layers_to_fetch.iter().map(|(l, _)| l.layer.size()).sum(), |
| 534 | + bytes_total: prep.all_layers().map(|l| l.layer.size()).sum(), |
| 535 | + digest: prep.manifest_digest.clone(), |
| 536 | + prep, |
| 537 | + }; |
| 538 | + |
| 539 | + Ok(PreparedPullResult::Ready(Box::new(prepared_image))) |
| 540 | +} |
| 541 | + |
| 542 | +/// Unified pull: Use podman to pull to containers-storage, then read from there |
| 543 | +/// |
| 544 | +/// The `sysroot_path` parameter specifies the path to the sysroot where bootc storage is located. |
| 545 | +/// For normal upgrade/switch operations, pass `None` to use the default `/sysroot`. |
| 546 | +pub(crate) async fn pull_unified( |
| 547 | + repo: &ostree::Repo, |
| 548 | + imgref: &ImageReference, |
| 549 | + target_imgref: Option<&OstreeImageReference>, |
| 550 | + quiet: bool, |
| 551 | + prog: ProgressWriter, |
| 552 | + store: &Storage, |
| 553 | + sysroot_path: Option<&camino::Utf8Path>, |
| 554 | +) -> Result<Box<ImageState>> { |
| 555 | + match prepare_for_pull_unified(repo, imgref, target_imgref, store, sysroot_path).await? { |
| 556 | + PreparedPullResult::AlreadyPresent(existing) => { |
| 557 | + // Log that the image was already present (Debug level since it's not actionable) |
| 558 | + const IMAGE_ALREADY_PRESENT_ID: &str = "5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9"; |
| 559 | + tracing::debug!( |
| 560 | + message_id = IMAGE_ALREADY_PRESENT_ID, |
| 561 | + bootc.image.reference = &imgref.image, |
| 562 | + bootc.image.transport = &imgref.transport, |
| 563 | + bootc.status = "already_present", |
| 564 | + "Image already present: {}", |
| 565 | + imgref |
| 566 | + ); |
| 567 | + Ok(existing) |
| 568 | + } |
| 569 | + PreparedPullResult::Ready(prepared_image_meta) => { |
| 570 | + // To avoid duplicate success logs, pass a containers-storage imgref to the importer |
| 571 | + let cs_imgref = ImageReference { |
| 572 | + transport: "containers-storage".to_string(), |
| 573 | + image: imgref.image.clone(), |
| 574 | + signature: imgref.signature.clone(), |
| 575 | + }; |
| 576 | + pull_from_prepared(&cs_imgref, quiet, prog, *prepared_image_meta).await |
| 577 | + } |
| 578 | + } |
| 579 | +} |
| 580 | + |
384 | 581 | #[context("Pulling")] |
385 | 582 | pub(crate) async fn pull_from_prepared( |
386 | 583 | imgref: &ImageReference, |
@@ -430,18 +627,21 @@ pub(crate) async fn pull_from_prepared( |
430 | 627 | let imgref_canonicalized = imgref.clone().canonicalize()?; |
431 | 628 | tracing::debug!("Canonicalized image reference: {imgref_canonicalized:#}"); |
432 | 629 |
|
433 | | - // Log successful import completion |
434 | | - const IMPORT_COMPLETE_JOURNAL_ID: &str = "4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8"; |
435 | | - |
436 | | - tracing::info!( |
437 | | - message_id = IMPORT_COMPLETE_JOURNAL_ID, |
438 | | - bootc.image.reference = &imgref.image, |
439 | | - bootc.image.transport = &imgref.transport, |
440 | | - bootc.manifest_digest = import.manifest_digest.as_ref(), |
441 | | - bootc.ostree_commit = &import.merge_commit, |
442 | | - "Successfully imported image: {}", |
443 | | - imgref |
444 | | - ); |
| 630 | + // Log successful import completion (skip if using unified storage to avoid double logging) |
| 631 | + let is_unified_path = imgref.transport == "containers-storage"; |
| 632 | + if !is_unified_path { |
| 633 | + const IMPORT_COMPLETE_JOURNAL_ID: &str = "4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8"; |
| 634 | + |
| 635 | + tracing::info!( |
| 636 | + message_id = IMPORT_COMPLETE_JOURNAL_ID, |
| 637 | + bootc.image.reference = &imgref.image, |
| 638 | + bootc.image.transport = &imgref.transport, |
| 639 | + bootc.manifest_digest = import.manifest_digest.as_ref(), |
| 640 | + bootc.ostree_commit = &import.merge_commit, |
| 641 | + "Successfully imported image: {}", |
| 642 | + imgref |
| 643 | + ); |
| 644 | + } |
445 | 645 |
|
446 | 646 | if let Some(msg) = |
447 | 647 | ostree_container::store::image_filtered_content_warning(&import.filtered_files) |
@@ -1051,7 +1251,7 @@ pub(crate) fn fixup_etc_fstab(root: &Dir) -> Result<()> { |
1051 | 1251 | } |
1052 | 1252 |
|
1053 | 1253 | // Read the input, and atomically write a modified version |
1054 | | - root.atomic_replace_with(fstab_path, move |mut w| { |
| 1254 | + root.atomic_replace_with(fstab_path, move |mut w| -> Result<()> { |
1055 | 1255 | for line in fd.lines() { |
1056 | 1256 | let line = line?; |
1057 | 1257 | if !edit_fstab_line(&line, &mut w)? { |
|
0 commit comments