|
15 | 15 | //! |
16 | 16 | //! ## `bootc image set-unified full` |
17 | 17 | //! |
18 | | -//! `set_unified_entrypoint` onboards the ostree backend via `set_unified`. |
19 | | -//! Composefs-native onboarding is provided separately by the |
20 | | -//! composefs-native feature. |
| 18 | +//! `set_unified_entrypoint` dispatches to `set_unified` (ostree backend) or |
| 19 | +//! `set_unified_composefs` (composefs backend). The composefs path: |
| 20 | +//! |
| 21 | +//! 1. Pulls the currently booted image into bootc-owned containers-storage |
| 22 | +//! (directly, or by copying from the host store to avoid the network). |
| 23 | +//! 2. Runs a full reconcile so that the rollback and staged images are |
| 24 | +//! backfilled too (see below). |
| 25 | +//! 3. *Only then* writes the `bootc.json` flag — so a failure in steps 1–2 |
| 26 | +//! leaves the system un-marked rather than half-migrated. |
21 | 27 | //! |
22 | 28 | //! After this, all subsequent upgrades/switches route through the forward |
23 | 29 | //! unified pipeline (cstorage → composefs reflink). On a reflink-capable |
@@ -1771,10 +1777,8 @@ pub(crate) async fn set_unified_entrypoint(enabled_with_copy: bool) -> Result<() |
1771 | 1777 |
|
1772 | 1778 | let storage = crate::cli::get_storage().await?; |
1773 | 1779 |
|
1774 | | - if let crate::store::BootedStorageKind::Composefs(_) = storage.kind()? { |
1775 | | - anyhow::bail!( |
1776 | | - "unified storage onboarding on the composefs-native backend requires the composefs-native feature" |
1777 | | - ); |
| 1780 | + if let crate::store::BootedStorageKind::Composefs(booted_cfs) = storage.kind()? { |
| 1781 | + return set_unified_composefs(&storage, &booted_cfs, local_fetch).await; |
1778 | 1782 | } |
1779 | 1783 |
|
1780 | 1784 | // Initialize floating c_storage early - needed for container operations |
@@ -1890,6 +1894,182 @@ async fn bind_ostree_composefs(sysroot: &crate::store::Storage) -> Result<()> { |
1890 | 1894 | Ok(()) |
1891 | 1895 | } |
1892 | 1896 |
|
| 1897 | +/// Composefs implementation of set_unified: pull the booted image into |
| 1898 | +/// bootc-owned containers-storage so future upgrades use the unified |
| 1899 | +/// (zero-copy) path automatically. |
| 1900 | +#[context("Setting unified storage for composefs")] |
| 1901 | +async fn set_unified_composefs( |
| 1902 | + storage: &crate::store::Storage, |
| 1903 | + booted_cfs: &crate::store::BootedComposefs, |
| 1904 | + local_fetch: composefs_ctl::composefs_oci::LocalFetchOpt, |
| 1905 | +) -> Result<()> { |
| 1906 | + use crate::bootc_composefs::status::get_composefs_status; |
| 1907 | + |
| 1908 | + const SET_UNIFIED_CFS_JOURNAL_ID: &str = "2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e"; |
| 1909 | + |
| 1910 | + let host = get_composefs_status(storage, booted_cfs) |
| 1911 | + .await |
| 1912 | + .context("Getting composefs deployment status")?; |
| 1913 | + |
| 1914 | + let imgref = host |
| 1915 | + .spec |
| 1916 | + .image |
| 1917 | + .as_ref() |
| 1918 | + .ok_or_else(|| anyhow::anyhow!("No image source specified for booted deployment"))?; |
| 1919 | + |
| 1920 | + tracing::info!( |
| 1921 | + message_id = SET_UNIFIED_CFS_JOURNAL_ID, |
| 1922 | + bootc.image.reference = &imgref.image, |
| 1923 | + bootc.image.transport = &imgref.transport, |
| 1924 | + "Pulling booted image into bootc containers-storage for unified storage: {}", |
| 1925 | + imgref, |
| 1926 | + ); |
| 1927 | + |
| 1928 | + let imgstore = storage.get_ensure_imgstore()?; |
| 1929 | + |
| 1930 | + // Pull into bootc-owned containers-storage if not already there. |
| 1931 | + let img_transport = imgref.to_transport_image()?; |
| 1932 | + if imgstore.exists(&img_transport).await? { |
| 1933 | + tracing::info!( |
| 1934 | + message_id = SET_UNIFIED_CFS_JOURNAL_ID, |
| 1935 | + bootc.status = "already_in_bootc_storage", |
| 1936 | + "Image already present in bootc containers-storage", |
| 1937 | + ); |
| 1938 | + } else { |
| 1939 | + // If the image exists in the host's default containers-storage |
| 1940 | + // (/var/lib/containers), copy from there (avoids network). |
| 1941 | + // Otherwise, pull from the original transport. |
| 1942 | + let image_in_host = image_exists_in_host_storage(&imgref.image).await?; |
| 1943 | + |
| 1944 | + if image_in_host { |
| 1945 | + tracing::info!( |
| 1946 | + "Image {} found in host containers-storage; copying to bootc storage", |
| 1947 | + &imgref.image |
| 1948 | + ); |
| 1949 | + let image_name = imgref.image.clone(); |
| 1950 | + let copy_msg = format!("Copying {} to bootc storage", &image_name); |
| 1951 | + async_task_with_spinner(©_msg, async move { |
| 1952 | + imgstore.pull_from_host_storage(&image_name).await |
| 1953 | + }) |
| 1954 | + .await?; |
| 1955 | + } else { |
| 1956 | + let pull_ref = img_transport; |
| 1957 | + let pull_msg = format!("Pulling {} to bootc storage", &pull_ref); |
| 1958 | + async_task_with_spinner(&pull_msg, async move { |
| 1959 | + imgstore.pull_with_progress(&pull_ref).await |
| 1960 | + }) |
| 1961 | + .await?; |
| 1962 | + } |
| 1963 | + |
| 1964 | + // Verify |
| 1965 | + let imgstore = storage.get_ensure_imgstore()?; |
| 1966 | + let img_transport = imgref.to_transport_image()?; |
| 1967 | + if !imgstore.exists(&img_transport).await? { |
| 1968 | + anyhow::bail!( |
| 1969 | + "Image was pulled but not found in bootc storage: {}", |
| 1970 | + &imgref.image |
| 1971 | + ); |
| 1972 | + } |
| 1973 | + } |
| 1974 | + |
| 1975 | + // Ensure the booted image has a bootc GC tag in the composefs repo so that |
| 1976 | + // reconcile_unified_storage (called next) can find it via list_refs. |
| 1977 | + // The rollback/staged images are tagged at deploy time by pull_via_composefs; |
| 1978 | + // only the booted image may be missing its tag on a freshly-onboarded system. |
| 1979 | + { |
| 1980 | + use crate::bootc_composefs::state::read_origin; |
| 1981 | + use crate::composefs_consts::{ORIGIN_KEY_IMAGE, ORIGIN_KEY_MANIFEST_DIGEST}; |
| 1982 | + use composefs_ctl::composefs_oci::tag_image; |
| 1983 | + |
| 1984 | + // The booted deployment ID is its fsverity digest (ComposefsCmdline.digest). |
| 1985 | + let deployment_id = &booted_cfs.cmdline.digest; |
| 1986 | + |
| 1987 | + // The manifest digest is stored in the .origin file by pull_via_composefs. |
| 1988 | + let ini = read_origin(&storage.physical_root, deployment_id) |
| 1989 | + .context("Reading booted deployment origin for composefs GC tag")?; |
| 1990 | + if let Some(ini) = ini { |
| 1991 | + if let Some(manifest_digest_str) = |
| 1992 | + ini.get::<String>(ORIGIN_KEY_IMAGE, ORIGIN_KEY_MANIFEST_DIGEST) |
| 1993 | + { |
| 1994 | + let manifest_digest: composefs_oci::OciDigest = manifest_digest_str |
| 1995 | + .parse() |
| 1996 | + .context("Parsing manifest digest from origin")?; |
| 1997 | + let cfs_repo = storage.get_ensure_composefs()?; |
| 1998 | + let tag = |
| 1999 | + crate::bootc_composefs::repo::bootc_tag_for_manifest(&manifest_digest_str); |
| 2000 | + |
| 2001 | + let existing_refs = |
| 2002 | + composefs_oci::list_refs(&*cfs_repo).context("Listing composefs refs")?; |
| 2003 | + if existing_refs.iter().any(|(t, _)| t == &tag) { |
| 2004 | + tracing::info!("Bootc GC tag {tag} already exists in composefs repo"); |
| 2005 | + } else { |
| 2006 | + tracing::info!("Writing bootc GC tag {tag} for booted image in composefs repo"); |
| 2007 | + tag_image(&*cfs_repo, &manifest_digest, &tag) |
| 2008 | + .context("Tagging booted image as bootc GC root in composefs repo")?; |
| 2009 | + } |
| 2010 | + } else { |
| 2011 | + tracing::warn!( |
| 2012 | + "No manifest digest in origin for deployment {deployment_id}; \ |
| 2013 | + bootc GC tag not written (legacy deployment?)" |
| 2014 | + ); |
| 2015 | + } |
| 2016 | + } else { |
| 2017 | + tracing::warn!( |
| 2018 | + "No origin file for deployment {deployment_id}; \ |
| 2019 | + bootc GC tag not written" |
| 2020 | + ); |
| 2021 | + } |
| 2022 | + } |
| 2023 | + |
| 2024 | + // Reconcile all live deployments (booted + rollback + staged) into |
| 2025 | + // containers-storage. This is the key step that makes fsck pass after |
| 2026 | + // set-unified on a system with a rollback deployment. |
| 2027 | + let summary = reconcile_unified_storage(storage).await?; |
| 2028 | + tracing::info!( |
| 2029 | + message_id = SET_UNIFIED_CFS_JOURNAL_ID, |
| 2030 | + bootc.reconcile.checked = summary.checked, |
| 2031 | + bootc.reconcile.restored = summary.restored, |
| 2032 | + bootc.reconcile.already_present = summary.already_present, |
| 2033 | + "Unified storage reconcile complete", |
| 2034 | + ); |
| 2035 | + if !summary.failures.is_empty() { |
| 2036 | + let msgs: Vec<String> = summary |
| 2037 | + .failures |
| 2038 | + .iter() |
| 2039 | + .map(|(name, e)| format!("{name}: {e:#}")) |
| 2040 | + .collect(); |
| 2041 | + bail!( |
| 2042 | + "Failed to reconcile {} image(s) into containers-storage:\n{}", |
| 2043 | + summary.failures.len(), |
| 2044 | + msgs.join("\n") |
| 2045 | + ); |
| 2046 | + } |
| 2047 | + |
| 2048 | + // Write composefs/bootc.json so pull_via_composefs routes through the |
| 2049 | + // three-store unified pipeline on all subsequent upgrades/switches. |
| 2050 | + { |
| 2051 | + use composefs_ctl::composefs_oci::LocalFetchOpt; |
| 2052 | + let mut meta = |
| 2053 | + crate::store::BootcRepoMeta::read(&storage.physical_root)?.unwrap_or_default(); |
| 2054 | + meta.version = 1; |
| 2055 | + meta.unified = if local_fetch == LocalFetchOpt::ZeroCopy { |
| 2056 | + crate::spec::UnifiedStorageState::Enabled |
| 2057 | + } else { |
| 2058 | + crate::spec::UnifiedStorageState::EnabledWithCopy |
| 2059 | + }; |
| 2060 | + meta.write(&storage.physical_root) |
| 2061 | + .context("Writing unified-storage flag to composefs/bootc.json")?; |
| 2062 | + } |
| 2063 | + |
| 2064 | + tracing::info!( |
| 2065 | + message_id = SET_UNIFIED_CFS_JOURNAL_ID, |
| 2066 | + bootc.status = "set_unified_complete", |
| 2067 | + "Unified storage set. Future upgrade/switch will use zero-copy path automatically.", |
| 2068 | + ); |
| 2069 | + println!("Unified storage enabled for {}.", imgref.image); |
| 2070 | + Ok(()) |
| 2071 | +} |
| 2072 | + |
1893 | 2073 | /// Inner implementation of set_unified for ostree that accepts a storage reference. |
1894 | 2074 | #[context("Setting unified storage for booted image")] |
1895 | 2075 | pub(crate) async fn set_unified( |
|
0 commit comments