Skip to content

Commit f123351

Browse files
committed
image: add composefs-native unified storage onboarding
Restores `set_unified_composefs` and the native dispatch arm in `set_unified_entrypoint` for `bootc image set-unified` on the composefs-native backend. Also restores the module-doc description of the composefs path and the updated bootc_composefs/mod.rs doc comment. This is the native-backend layer stacked atop the ostree-only `unified-ostree` branch. Together they reconstruct the full unified storage feature (equivalent to b213097b8d). Assisted-by: OpenCode (Claude Sonnet 4.6) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 0307111 commit f123351

2 files changed

Lines changed: 205 additions & 7 deletions

File tree

crates/lib/src/bootc_composefs/mod.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
1+
//! The composefs boot backend.
2+
//!
3+
//! This module and its submodules implement bootc's composefs-native boot
4+
//! backend, where the OS image is stored and booted directly from a composefs
5+
//! EROFS overlay rather than through an ostree deployment.
6+
//!
7+
//! ## Submodule layout
8+
//!
9+
//! | Submodule | Responsibility |
10+
//! |-----------|----------------|
11+
//! | `repo` / `update` | Pull images into the composefs object store; handle upgrades and switches |
12+
//! | `status` | Read deployment state from bootloader entries and composefs origin files |
13+
//! | `export` | Export composefs-repo images to OCI layout for reconcile / registry push |
14+
//! | `gc` | Garbage-collect unreferenced composefs tags and splitstreams |
15+
//! | `boot` / `finalize` | Prepare the boot environment (BLS entries, UKI, TPM sealing) |
16+
//! | `selinux` | SELinux policy compatibility checks for in-place updates |
17+
//! | `soft_reboot` | systemd-soft-reboot support for zero-downtime image switches |
18+
119
pub(crate) mod backwards_compat;
220
pub(crate) mod boot;
321
pub(crate) mod delete;

crates/lib/src/image.rs

Lines changed: 187 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,15 @@
1515
//!
1616
//! ## `bootc image set-unified full`
1717
//!
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.
2127
//!
2228
//! After this, all subsequent upgrades/switches route through the forward
2329
//! 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<()
17711777

17721778
let storage = crate::cli::get_storage().await?;
17731779

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;
17781782
}
17791783

17801784
// Initialize floating c_storage early - needed for container operations
@@ -1890,6 +1894,182 @@ async fn bind_ostree_composefs(sysroot: &crate::store::Storage) -> Result<()> {
18901894
Ok(())
18911895
}
18921896

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(&copy_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+
18932073
/// Inner implementation of set_unified for ostree that accepts a storage reference.
18942074
#[context("Setting unified storage for booted image")]
18952075
pub(crate) async fn set_unified(

0 commit comments

Comments
 (0)