Skip to content

Commit 0a37718

Browse files
committed
Respect bootloader=none in flat mode; add test
- flat_install() now checks config_opts.bootloader for Bootloader::None and skips bootloader installation, matching the ostree path behaviour. This enables running the flat install test in CI without a real EFI partition/bootupd. - Add InstallFlat subcommand to tests-integration and a "flat install to-filesystem" trial that: * Creates a 5 GiB loopback ext4 target * Runs bootc install to-filesystem --bootloader=none --flat * Verifies etc/.bootc-flat marker, absence of ostree/, presence of boot/loader/entries/flat-*.conf and boot/vmlinuz-* Signed-off-by: Eric Curtin <eric.curtin@docker.com>
1 parent 12b460e commit 0a37718

4 files changed

Lines changed: 187 additions & 35 deletions

File tree

crates/lib/src/install.rs

Lines changed: 83 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1830,16 +1830,17 @@ async fn install_with_sysroot(
18301830

18311831
if cfg!(target_arch = "s390x") {
18321832
// TODO: Integrate s390x support into install_via_bootupd
1833-
crate::bootloader::install_via_zipl(&rootfs.device_info, boot_uuid)?;
1833+
install_bootloader_via_zipl(&rootfs.device_info, boot_uuid)?;
18341834
} else {
18351835
match postfetch.detected_bootloader {
18361836
Bootloader::Grub => {
1837-
crate::bootloader::install_via_bootupd(
1837+
let target_root = rootfs
1838+
.target_root_path
1839+
.as_ref()
1840+
.unwrap_or(&rootfs.physical_root_path);
1841+
install_bootloader_via_bootupd(
18381842
&rootfs.device_info,
1839-
&rootfs
1840-
.target_root_path
1841-
.clone()
1842-
.unwrap_or(rootfs.physical_root_path.clone()),
1843+
target_root,
18431844
&state.config_opts,
18441845
Some(&deployment_path.as_str()),
18451846
)?;
@@ -1875,6 +1876,35 @@ async fn install_with_sysroot(
18751876
Ok(())
18761877
}
18771878

1879+
/// Install the bootloader using bootupd.
1880+
///
1881+
/// This is a helper to reduce duplication between ostree and flat installs.
1882+
///
1883+
/// # Arguments
1884+
/// * `device_info` - Device information for the target
1885+
/// * `target_root` - Path to the target root filesystem
1886+
/// * `config_opts` - Configuration options
1887+
/// * `deployment_path` - Optional deployment path for ostree-based installs
1888+
#[context("Installing bootloader via bootupd")]
1889+
fn install_bootloader_via_bootupd(
1890+
device_info: &DeviceSetup,
1891+
target_root: &Utf8PathBuf,
1892+
config_opts: &ConfigOpts,
1893+
deployment_path: Option<&str>,
1894+
) -> Result<()> {
1895+
crate::bootloader::install_via_bootupd(device_info, target_root, config_opts, deployment_path)
1896+
}
1897+
1898+
/// Install the bootloader using zipl (s390x architecture).
1899+
///
1900+
/// # Arguments
1901+
/// * `device_info` - Device information for the target
1902+
/// * `boot_uuid` - Boot UUID (required for zipl)
1903+
#[context("Installing bootloader via zipl")]
1904+
fn install_bootloader_via_zipl(device_info: &DeviceSetup, boot_uuid: &str) -> Result<()> {
1905+
crate::bootloader::install_via_zipl(device_info, boot_uuid)
1906+
}
1907+
18781908
enum BoundImages {
18791909
Skip,
18801910
Resolved(Vec<ResolvedBoundImage>),
@@ -1946,20 +1976,26 @@ fn copy_kernel_to_boot(
19461976
let vmlinuz_dest = format!("boot/vmlinuz-{version}");
19471977
let initramfs_dest = format!("boot/initramfs-{version}.img");
19481978

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}"))?;
1979+
// Copy vmlinuz using streaming to avoid loading entire file into memory
1980+
let mut vmlinuz_f = root
1981+
.open(path.as_str())
1982+
.with_context(|| format!("Opening kernel {path}"))?;
1983+
root.atomic_write_with(&vmlinuz_dest, |to_f| {
1984+
std::io::copy(&mut vmlinuz_f, to_f)?;
1985+
Ok(())
1986+
})
1987+
.with_context(|| format!("Writing {vmlinuz_dest}"))?;
19551988

19561989
// Copy initramfs (it may not exist; dracut will regenerate it if missing)
19571990
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}"))?;
1991+
let mut initramfs_f = root
1992+
.open(initramfs.as_str())
1993+
.with_context(|| format!("Opening initramfs {initramfs}"))?;
1994+
root.atomic_write_with(&initramfs_dest, |to_f| {
1995+
std::io::copy(&mut initramfs_f, to_f)?;
1996+
Ok(())
1997+
})
1998+
.with_context(|| format!("Writing {initramfs_dest}"))?;
19631999
}
19642000

19652001
// Return absolute paths for use in BLS entry
@@ -2071,26 +2107,38 @@ async fn flat_install(state: &State, rootfs: &RootSetup) -> Result<()> {
20712107
regenerate_initramfs_for_flat(&target_path, &kernel_version, &initramfs_boot_path)?;
20722108

20732109
// Step 5: Create BLS entry
2074-
create_flat_bls_entry(rootfs, &kernel_version, &vmlinuz_boot_path, &initramfs_boot_path)?;
2110+
create_flat_bls_entry(
2111+
rootfs,
2112+
&kernel_version,
2113+
&vmlinuz_boot_path,
2114+
&initramfs_boot_path,
2115+
)?;
20752116

20762117
// 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-
)?;
2118+
match state.config_opts.bootloader.as_ref() {
2119+
Some(crate::spec::Bootloader::None) => {
2120+
tracing::debug!("Skipping bootloader installation (bootloader=none)");
2121+
}
2122+
_ => {
2123+
if cfg!(target_arch = "s390x") {
2124+
let boot_uuid = rootfs
2125+
.get_boot_uuid()?
2126+
.or(rootfs.rootfs_uuid.as_deref())
2127+
.ok_or_else(|| anyhow!("No uuid for boot/root"))?;
2128+
install_bootloader_via_zipl(&rootfs.device_info, boot_uuid)?;
2129+
} else {
2130+
let target_root = rootfs
2131+
.target_root_path
2132+
.as_ref()
2133+
.unwrap_or(&rootfs.physical_root_path);
2134+
install_bootloader_via_bootupd(
2135+
&rootfs.device_info,
2136+
target_root,
2137+
&state.config_opts,
2138+
None, // No deployment path for flat installs
2139+
)?;
2140+
}
2141+
}
20942142
}
20952143

20962144
// Step 7: Write flat install marker

crates/tests-integration/src/install.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,79 @@ pub(crate) fn run_alongside(image: &str, mut testargs: libtest_mimic::Arguments)
173173

174174
libtest_mimic::run(&testargs, tests.into()).exit()
175175
}
176+
177+
#[context("Flat install tests")]
178+
pub(crate) fn run_flat(image: &str, mut testargs: libtest_mimic::Arguments) -> Result<()> {
179+
testargs.test_threads = Some(1);
180+
let image: &'static str = String::from(image).leak();
181+
182+
let tests = [Trial::test("flat install to-filesystem", move || {
183+
let sh = &xshell::Shell::new()?;
184+
// Create a sparse file and format it as ext4
185+
let size = 5 * 1000 * 1000 * 1000u64;
186+
let mut tmpdisk = tempfile::NamedTempFile::new_in("/var/tmp")?;
187+
tmpdisk.as_file_mut().set_len(size)?;
188+
let tmpdisk = tmpdisk.into_temp_path();
189+
let tmpdisk_str = tmpdisk.to_str().unwrap();
190+
191+
// Set up loop device and format
192+
let loopdev = cmd!(sh, "sudo losetup --find --show {tmpdisk_str}")
193+
.read()?
194+
.trim()
195+
.to_string();
196+
cmd!(sh, "sudo mkfs.ext4 -L root {loopdev}").run()?;
197+
198+
// Mount the target
199+
let tmpdir = tempfile::TempDir::new_in("/var/tmp")?;
200+
let target = tmpdir.path().to_str().unwrap();
201+
cmd!(sh, "sudo mount {loopdev} {target}").run()?;
202+
203+
// Run flat install (skip bootloader for CI) and capture result for cleanup
204+
let r = (|| -> Result<()> {
205+
cmd!(sh, "sudo {BASE_ARGS...} -v {target}:/target {image} bootc install to-filesystem --bootloader=none --flat /target").run()?;
206+
207+
// Verify flat install marker
208+
assert!(
209+
std::path::Path::new(target)
210+
.join("etc/.bootc-flat")
211+
.exists(),
212+
"Missing flat install marker"
213+
);
214+
215+
// Verify no ostree directory was created
216+
assert!(
217+
!std::path::Path::new(target).join("ostree").exists(),
218+
"ostree directory should not exist in flat install"
219+
);
220+
221+
// Verify boot entries exist
222+
let boot_entries = std::path::Path::new(target).join("boot/loader/entries");
223+
let has_entry = boot_entries.exists()
224+
&& std::fs::read_dir(&boot_entries)?.any(|e| {
225+
e.ok()
226+
.map(|e| e.file_name().to_string_lossy().contains("flat-"))
227+
.unwrap_or(false)
228+
});
229+
assert!(has_entry, "No flat BLS entry found in boot/loader/entries");
230+
231+
// Verify vmlinuz was copied to /boot
232+
let has_vmlinuz =
233+
std::fs::read_dir(std::path::Path::new(target).join("boot"))?.any(|e| {
234+
e.ok()
235+
.map(|e| e.file_name().to_string_lossy().starts_with("vmlinuz-"))
236+
.unwrap_or(false)
237+
});
238+
assert!(has_vmlinuz, "No vmlinuz-* found in /boot");
239+
240+
Ok(())
241+
})();
242+
243+
// Clean up regardless of result
244+
let _ = cmd!(sh, "sudo umount --lazy {target}").run();
245+
let _ = cmd!(sh, "sudo losetup --detach {loopdev}").run();
246+
247+
Ok(r?)
248+
})];
249+
250+
libtest_mimic::run(&testargs, tests.into()).exit()
251+
}

crates/tests-integration/src/tests-integration.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ pub(crate) enum Opt {
2626
#[clap(flatten)]
2727
testargs: libtest_mimic::Arguments,
2828
},
29+
InstallFlat {
30+
/// Source container image reference
31+
image: String,
32+
#[clap(flatten)]
33+
testargs: libtest_mimic::Arguments,
34+
},
2935
HostPrivileged {
3036
image: String,
3137
#[clap(flatten)]
@@ -54,6 +60,7 @@ fn main() {
5460
let r = match opt {
5561
Opt::SystemReinstall { image, testargs } => system_reinstall::run(&image, testargs),
5662
Opt::InstallAlongside { image, testargs } => install::run_alongside(&image, testargs),
63+
Opt::InstallFlat { image, testargs } => install::run_flat(&image, testargs),
5764
Opt::HostPrivileged { image, testargs } => hostpriv::run_hostpriv(&image, testargs),
5865
Opt::Container { testargs } => container::run(testargs),
5966
Opt::RunVM(opts) => runvm::run(opts),

hack/Containerfile.flat-test

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Build a test image with --flat support layered on fedora-bootc:43
2+
# Stage 1: build bootc binary on Fedora 43 (matching target libraries)
3+
FROM quay.io/fedora/fedora-bootc:43 AS builder
4+
5+
RUN dnf -y install cargo rust gcc openssl-devel pkg-config perl \
6+
ostree-devel glib2-devel libzstd-devel make skopeo \
7+
&& dnf clean all
8+
9+
COPY . /src
10+
WORKDIR /src
11+
12+
# Build only the bootc binary (release mode for speed)
13+
RUN --mount=type=cache,target=/root/.cargo/registry \
14+
--mount=type=cache,target=/src/target \
15+
cargo build -p bootc --release && \
16+
cp target/release/bootc /usr/local/bin/bootc-flat
17+
18+
# Stage 2: final image is fedora-bootc:43 with our bootc
19+
FROM quay.io/fedora/fedora-bootc:43
20+
21+
COPY --from=builder /usr/local/bin/bootc-flat /usr/bin/bootc

0 commit comments

Comments
 (0)