Skip to content

Commit 12b460e

Browse files
committed
Add --flat mode for to-filesystem installation
Implement an experimental "flat" install mode for `bootc install to-filesystem`. When `--flat` is passed, the container rootfs is copied directly to the target filesystem without ostree or composefs layering. Post-install, the system boots as a regular (non-bootc) Linux system; day-2 operations (upgrade, rollback) are unavailable. This addresses the "step 1" approach described in #1668: getting bootc to perform a clean traditional-style OS install from a container image. Changes: - Add `FLAT_INSTALL_MARKER` constant (`etc/.bootc-flat`) - Add `--flat` flag to `InstallTargetFilesystemOpts` - Add `flat: bool` field to `RootSetup` (defaulting to false in all existing construction sites including baseline.rs) - Branch in `install_to_filesystem_impl` to call `flat_install()` before the composefs/ostree paths - Implement `flat_install()` which: 1. Copies container rootfs via `cp --archive --one-file-system` 2. Finds the kernel using the existing `kernel::find_kernel()` API 3. Copies vmlinuz and initramfs to `/boot/vmlinuz-<ver>` etc. 4. Regenerates initramfs via dracut (omitting ostree modules), with graceful degradation if dracut is absent in the target 5. Creates a BLS entry at `boot/loader/entries/flat-<ver>.conf` 6. Installs the bootloader via the existing bootupd/zipl paths 7. Writes the flat install marker - Add unit test for `create_flat_bls_entry` verifying the generated BLS config format Only self-install mode (running inside the container) is supported; `--source-imgref` is rejected with a clear error. UKI kernels are not yet supported (rejected with a clear error). Signed-off-by: Eric Curtin <eric.curtin@docker.com>
1 parent 7a5ce1e commit 12b460e

2 files changed

Lines changed: 265 additions & 2 deletions

File tree

crates/lib/src/install.rs

Lines changed: 264 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ use std::time::Duration;
161161
use aleph::InstallAleph;
162162
use anyhow::{Context, Result, anyhow, ensure};
163163
use bootc_kernel_cmdline::utf8::{Cmdline, CmdlineOwned};
164-
use bootc_utils::CommandRunExt;
164+
use bootc_utils::{BwrapCmd, CommandRunExt};
165165
use camino::Utf8Path;
166166
use camino::Utf8PathBuf;
167167
use canon_json::CanonJsonSerialize;
@@ -238,6 +238,9 @@ const DEFAULT_REPO_CONFIG: &[(&str, &str)] = &[
238238
/// Kernel argument used to specify we want the rootfs mounted read-write by default
239239
pub(crate) const RW_KARG: &str = "rw";
240240

241+
/// Marker file written to the target root to indicate a flat (non-ostree) install was performed.
242+
pub(crate) const FLAT_INSTALL_MARKER: &str = "etc/.bootc-flat";
243+
241244
#[derive(clap::Args, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
242245
pub(crate) struct InstallTargetOpts {
243246
// TODO: A size specifier which allocates free space for the root in *addition* to the base container image size
@@ -488,6 +491,13 @@ pub(crate) struct InstallTargetFilesystemOpts {
488491
/// is then the responsibility of the invoking code to perform those operations.
489492
#[clap(long)]
490493
pub(crate) skip_finalize: bool,
494+
495+
/// Install in "flat" mode: the container rootfs is copied directly to the target filesystem
496+
/// without ostree or composefs layering. This is experimental. Post-install, bootc day-2
497+
/// operations (upgrade, rollback, etc.) are unavailable on the installed system.
498+
/// Requires running inside the container to install (self-install mode).
499+
#[clap(long)]
500+
pub(crate) flat: bool,
491501
}
492502

493503
#[derive(Debug, Clone, clap::Parser, PartialEq, Eq)]
@@ -1286,6 +1296,8 @@ pub(crate) struct RootSetup {
12861296
skip_finalize: bool,
12871297
boot: Option<MountSpec>,
12881298
pub(crate) kargs: CmdlineOwned,
1299+
/// If true, perform a flat installation (no ostree/composefs)
1300+
pub(crate) flat: bool,
12891301
}
12901302

12911303
fn require_boot_uuid(spec: &MountSpec) -> Result<&str> {
@@ -1899,6 +1911,197 @@ impl BoundImages {
18991911
}
19001912
}
19011913

1914+
/// Copy the running container's rootfs to a target directory using `cp --archive`.
1915+
async fn copy_container_rootfs_to_target(target_path: &Utf8PathBuf) -> Result<()> {
1916+
let target_path_str = target_path.to_string();
1917+
crate::utils::async_task_with_spinner(
1918+
"Copying container rootfs to target",
1919+
tokio::task::spawn_blocking(move || {
1920+
Task::new("Copying rootfs", "cp")
1921+
.args([
1922+
"--archive",
1923+
"--one-file-system",
1924+
"--no-target-directory",
1925+
"/",
1926+
&target_path_str,
1927+
])
1928+
.run()
1929+
}),
1930+
)
1931+
.await?
1932+
}
1933+
1934+
/// Copy vmlinuz and initramfs from `usr/lib/modules/<version>/` to `boot/` in the target root.
1935+
///
1936+
/// Returns the absolute-path strings suitable for use in a BLS config entry, e.g.
1937+
/// (`/boot/vmlinuz-<ver>`, `/boot/initramfs-<ver>.img`).
1938+
#[context("Copying kernel to /boot")]
1939+
fn copy_kernel_to_boot(
1940+
root: &Dir,
1941+
kernel: &crate::kernel::KernelInternal,
1942+
) -> Result<(Utf8PathBuf, Utf8PathBuf)> {
1943+
let version = &kernel.kernel.version;
1944+
match &kernel.k_type {
1945+
crate::kernel::KernelType::Vmlinuz { path, initramfs } => {
1946+
let vmlinuz_dest = format!("boot/vmlinuz-{version}");
1947+
let initramfs_dest = format!("boot/initramfs-{version}.img");
1948+
1949+
// Copy vmlinuz
1950+
let vmlinuz_data = root
1951+
.read(path.as_str())
1952+
.with_context(|| format!("Reading kernel {path}"))?;
1953+
root.atomic_write(&vmlinuz_dest, &vmlinuz_data)
1954+
.with_context(|| format!("Writing {vmlinuz_dest}"))?;
1955+
1956+
// Copy initramfs (it may not exist; dracut will regenerate it if missing)
1957+
if root.try_exists(initramfs.as_str())? {
1958+
let initramfs_data = root
1959+
.read(initramfs.as_str())
1960+
.with_context(|| format!("Reading initramfs {initramfs}"))?;
1961+
root.atomic_write(&initramfs_dest, &initramfs_data)
1962+
.with_context(|| format!("Writing {initramfs_dest}"))?;
1963+
}
1964+
1965+
// Return absolute paths for use in BLS entry
1966+
Ok((
1967+
Utf8PathBuf::from(format!("/boot/vmlinuz-{version}")),
1968+
Utf8PathBuf::from(format!("/boot/initramfs-{version}.img")),
1969+
))
1970+
}
1971+
crate::kernel::KernelType::Uki { .. } => {
1972+
anyhow::bail!("Flat install with UKI kernels is not yet supported")
1973+
}
1974+
}
1975+
}
1976+
1977+
/// Run dracut inside the target to regenerate the initramfs, omitting ostree-specific modules.
1978+
///
1979+
/// If dracut is not found in the target the warning is emitted and we continue.
1980+
fn regenerate_initramfs_for_flat(
1981+
target_path: &Utf8PathBuf,
1982+
kernel_version: &str,
1983+
initramfs_boot_path: &Utf8PathBuf,
1984+
) -> Result<()> {
1985+
// Check if dracut is available in the target
1986+
let has_dracut = target_path.join("usr/bin/dracut").try_exists()?
1987+
|| target_path.join("usr/sbin/dracut").try_exists()?;
1988+
if !has_dracut {
1989+
crate::utils::medium_visibility_warning(
1990+
"dracut not found in target; initramfs may require manual regeneration",
1991+
);
1992+
return Ok(());
1993+
}
1994+
1995+
println!("Regenerating initramfs (omitting ostree modules)");
1996+
BwrapCmd::new(target_path)
1997+
.run([
1998+
"dracut",
1999+
"--force",
2000+
"--no-hostonly",
2001+
"--omit",
2002+
"ostree",
2003+
initramfs_boot_path.as_str(),
2004+
kernel_version,
2005+
])
2006+
.context("Regenerating initramfs via dracut")
2007+
}
2008+
2009+
/// Write a BLS entry for a flat (non-ostree) installation.
2010+
#[context("Creating flat BLS entry")]
2011+
fn create_flat_bls_entry(
2012+
rootfs: &RootSetup,
2013+
kernel_version: &str,
2014+
vmlinuz_boot_path: &Utf8PathBuf,
2015+
initramfs_boot_path: &Utf8PathBuf,
2016+
) -> Result<()> {
2017+
use crate::parsers::bls_config::{BLSConfig, BLSConfigType};
2018+
2019+
let mut cfg = BLSConfig::default();
2020+
cfg.with_title(format!("Linux {kernel_version}"))
2021+
.with_version(kernel_version.to_string())
2022+
.with_cfg(BLSConfigType::NonEFI {
2023+
linux: vmlinuz_boot_path.clone(),
2024+
initrd: vec![initramfs_boot_path.clone()],
2025+
options: Some(rootfs.kargs.clone()),
2026+
});
2027+
2028+
let entry_path = format!("boot/loader/entries/flat-{kernel_version}.conf");
2029+
rootfs
2030+
.physical_root
2031+
.create_dir_all("boot/loader/entries")
2032+
.context("Creating boot/loader/entries")?;
2033+
2034+
let content = format!("{cfg}");
2035+
rootfs
2036+
.physical_root
2037+
.atomic_write(&entry_path, content.as_bytes())
2038+
.with_context(|| format!("Writing BLS entry {entry_path}"))?;
2039+
2040+
tracing::debug!("Wrote BLS entry: {entry_path}");
2041+
Ok(())
2042+
}
2043+
2044+
/// Perform a flat (non-ostree) installation: copy the container rootfs directly to the target
2045+
/// filesystem, then set up kernel, initramfs, BLS entry, and bootloader.
2046+
#[context("Performing flat install")]
2047+
async fn flat_install(state: &State, rootfs: &RootSetup) -> Result<()> {
2048+
if !state.source.in_host_mountns {
2049+
anyhow::bail!(
2050+
"--flat mode requires running inside the container to install; \
2051+
--source-imgref is not yet supported with --flat"
2052+
);
2053+
}
2054+
2055+
let target_path = rootfs.physical_root_path.clone();
2056+
println!("Installing in flat mode (experimental)");
2057+
2058+
// Step 1: Copy container rootfs to target
2059+
copy_container_rootfs_to_target(&target_path).await?;
2060+
2061+
// Step 2: Find kernel in the target root
2062+
let kernel = crate::kernel::find_kernel(&rootfs.physical_root)?
2063+
.ok_or_else(|| anyhow!("No kernel found in flat install target"))?;
2064+
let kernel_version = kernel.kernel.version.clone();
2065+
2066+
// Step 3: Copy kernel and initramfs to /boot/<ver>
2067+
let (vmlinuz_boot_path, initramfs_boot_path) =
2068+
copy_kernel_to_boot(&rootfs.physical_root, &kernel)?;
2069+
2070+
// Step 4: Regenerate initramfs via dracut (omit ostree modules)
2071+
regenerate_initramfs_for_flat(&target_path, &kernel_version, &initramfs_boot_path)?;
2072+
2073+
// Step 5: Create BLS entry
2074+
create_flat_bls_entry(rootfs, &kernel_version, &vmlinuz_boot_path, &initramfs_boot_path)?;
2075+
2076+
// Step 6: Install bootloader
2077+
if cfg!(target_arch = "s390x") {
2078+
let boot_uuid = rootfs
2079+
.get_boot_uuid()?
2080+
.or(rootfs.rootfs_uuid.as_deref())
2081+
.ok_or_else(|| anyhow!("No uuid for boot/root"))?;
2082+
crate::bootloader::install_via_zipl(&rootfs.device_info, boot_uuid)?;
2083+
} else {
2084+
let target_root = rootfs
2085+
.target_root_path
2086+
.as_ref()
2087+
.unwrap_or(&rootfs.physical_root_path);
2088+
crate::bootloader::install_via_bootupd(
2089+
&rootfs.device_info,
2090+
target_root,
2091+
&state.config_opts,
2092+
None, // No deployment path for flat installs
2093+
)?;
2094+
}
2095+
2096+
// Step 7: Write flat install marker
2097+
rootfs
2098+
.physical_root
2099+
.atomic_write(FLAT_INSTALL_MARKER, b"")
2100+
.context("Writing flat install marker")?;
2101+
2102+
Ok(())
2103+
}
2104+
19022105
async fn ostree_install(state: &State, rootfs: &RootSetup, cleanup: Cleanup) -> Result<()> {
19032106
// We verify this upfront because it's currently required by bootupd
19042107
let boot_uuid = rootfs
@@ -1971,7 +2174,9 @@ async fn install_to_filesystem_impl(
19712174
}
19722175
}
19732176

1974-
if state.composefs_options.composefs_backend {
2177+
if rootfs.flat {
2178+
flat_install(state, rootfs).await?;
2179+
} else if state.composefs_options.composefs_backend {
19752180
// Load a fd for the mounted target physical root
19762181

19772182
let (id, verity) = initialize_composefs_repository(
@@ -2608,6 +2813,7 @@ pub(crate) async fn install_to_filesystem(
26082813
boot,
26092814
kargs,
26102815
skip_finalize,
2816+
flat: fsopts.flat,
26112817
};
26122818

26132819
install_to_filesystem_impl(&state, &mut rootfs, cleanup).await?;
@@ -2658,6 +2864,7 @@ pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) ->
26582864
replace: opts.replace,
26592865
skip_finalize: true,
26602866
acknowledge_destructive: opts.acknowledge_destructive,
2867+
flat: false,
26612868
},
26622869
source_opts: opts.source_opts,
26632870
target_opts: opts.target_opts,
@@ -3050,4 +3257,59 @@ UUID=boot-uuid /boot ext4 defaults 0 0
30503257

30513258
Ok(())
30523259
}
3260+
3261+
#[test]
3262+
fn test_create_flat_bls_entry() -> Result<()> {
3263+
let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3264+
td.create_dir_all("boot")?;
3265+
3266+
let rootfs = RootSetup {
3267+
#[cfg(feature = "install-to-disk")]
3268+
luks_device: None,
3269+
device_info: bootc_blockdev::Device {
3270+
name: "vda".to_string(),
3271+
serial: None,
3272+
model: None,
3273+
partlabel: None,
3274+
parttype: None,
3275+
partuuid: None,
3276+
partn: None,
3277+
children: None,
3278+
size: 0,
3279+
maj_min: None,
3280+
start: None,
3281+
label: None,
3282+
fstype: None,
3283+
uuid: None,
3284+
path: None,
3285+
pttype: None,
3286+
},
3287+
physical_root_path: Utf8PathBuf::from("/test"),
3288+
physical_root: td.try_clone()?,
3289+
target_root_path: None,
3290+
rootfs_uuid: None,
3291+
skip_finalize: false,
3292+
boot: None,
3293+
kargs: CmdlineOwned::from("root=UUID=abc123 rw"),
3294+
flat: true,
3295+
};
3296+
3297+
let kernel_version = "6.12.0-100.fc41.x86_64";
3298+
let vmlinuz = Utf8PathBuf::from(format!("/boot/vmlinuz-{kernel_version}"));
3299+
let initramfs = Utf8PathBuf::from(format!("/boot/initramfs-{kernel_version}.img"));
3300+
3301+
create_flat_bls_entry(&rootfs, kernel_version, &vmlinuz, &initramfs)?;
3302+
3303+
// Read back the BLS entry and verify its contents
3304+
let entry_path = format!("boot/loader/entries/flat-{kernel_version}.conf");
3305+
let content = String::from_utf8(td.read(&entry_path)?)?;
3306+
3307+
assert!(content.contains(&format!("title Linux {kernel_version}")));
3308+
assert!(content.contains(&format!("version {kernel_version}")));
3309+
assert!(content.contains(&format!("linux /boot/vmlinuz-{kernel_version}")));
3310+
assert!(content.contains(&format!("initrd /boot/initramfs-{kernel_version}.img")));
3311+
assert!(content.contains("options root=UUID=abc123 rw"));
3312+
3313+
Ok(())
3314+
}
30533315
}

crates/lib/src/install/baseline.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,5 +488,6 @@ pub(crate) fn install_create_rootfs(
488488
boot,
489489
kargs,
490490
skip_finalize: false,
491+
flat: false,
491492
})
492493
}

0 commit comments

Comments
 (0)