diff --git a/Dockerfile b/Dockerfile index 9831efcca..7f6b889e6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,12 +46,81 @@ RUN --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp --mount=type=c # local sources. We'll override it later. # NOTE: All your base belong to me. FROM $base as target-base + +# SKIP_CONFIGS=1 skips LBIs, test kargs, and install configs (for FCOS testing) +ARG SKIP_CONFIGS +ARG boot_type +ARG seal_state +ARG variant +ARG baseconfigs="" +ARG rootfs="" + # Handle version skew between base image and mirrors for CentOS Stream # xref https://gitlab.com/redhat/centos-stream/containers/bootc/-/issues/1174 RUN --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ --mount=type=bind,from=packaging,src=/,target=/run/packaging \ /run/packaging/enable-compose-repos -RUN --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp /usr/libexec/bootc-base-imagectl build-rootfs --manifest=standard /target-rootfs + +RUN --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ + --mount=type=bind,from=src,src=/src/hack,target=/run/hack \ + --mount=type=bind,from=packaging,src=/,target=/run/packaging <&2 - exit 1 -fi +kver=$1 +shift # Create the EFI directory structure mkdir -p /boot/EFI/Linux @@ -36,12 +31,6 @@ mkdir -p /boot/EFI/Linux target=/boot/EFI/Linux/${kver}.efi cp "${uki_src}/${kver}.efi" "${target}" -# Remove the raw kernel and initramfs since we're using a UKI now. -# NOTE: We intentionally keep these for now until bcvk is updated to extract -# kernel/initramfs from UKIs in subdirectories. Once bcvk PR #144 is fixed -# to look for .efi files in /usr/lib/modules//, we can uncomment this. -# rm -v "/usr/lib/modules/${kver}/vmlinuz" "/usr/lib/modules/${kver}/initramfs.img" - # NOTE: We used to create a symlink from /usr/lib/modules/${kver}/${kver}.efi to the UKI # for tooling compatibility. However, composefs-boot's find_uki_components() doesn't # handle symlinks correctly and fails with "is not a regular file". The UKI is already diff --git a/contrib/packaging/seal-uki b/contrib/packaging/seal-uki index 66de92ffd..bfc7c4bec 100755 --- a/contrib/packaging/seal-uki +++ b/contrib/packaging/seal-uki @@ -2,32 +2,70 @@ # Generate a sealed UKI with embedded composefs digest set -xeuo pipefail -# Path to the desired root filesystem -target=$1 -shift -# Write to this directory -output=$1 -shift -# Path to secrets directory -secrets=$1 -shift -allow_missing_verity=$1 -shift -seal_state=$1 -shift - -if [[ $seal_state == "sealed" && $allow_missing_verity == "true" ]]; then +missing_verity=() + +while [ ! -z "${1:-}" ]; do + case "$1" in + # Path to the desired root filesystem + "--target") + target="$2" + shift + shift + ;; + + # Write to this directory + "--output") + output="$2" + shift + shift + ;; + + # Path to secrets directory + "--secrets") + secrets="$2" + shift + shift + ;; + + "--allow-missing-verity") + missing_verity=(--allow-missing-verity) + shift + ;; + + "--seal-state") + seal_state="$2" + shift + shift + ;; + + # Path to the directory containing kernel and initramfs + "--kernel-dir") + kernel_dir="$2" + shift + shift + ;; + + * ) + echo "Argument $1 not understood" + exit 1 + ;; + esac +done + +if [[ $seal_state == "sealed" && ${#missing_verity[@]} -gt 0 ]]; then echo "Cannot have missing verity with sealed UKI" >&2 exit 1 fi -# Find the kernel version (needed for output filename) -kver=$(bootc container inspect --rootfs "${target}" --json | jq -r '.kernel.version') -if [ -z "$kver" ] || [ "$kver" = "null" ]; then - echo "Error: No kernel found" >&2 - exit 1 +if [[ -z $kernel_dir ]]; then + echo "kernel dir is required" >&2 + exit 1 fi +kernel_params=(--kernel-dir "$kernel_dir") + +kver=$(basename "$kernel_dir") + mkdir -p "${output}" # Baseline ukify options @@ -45,12 +83,6 @@ fi # Baseline container ukify options containerukifyargs=(--rootfs "${target}") -missing_verity=() - -if [[ $allow_missing_verity == "true" ]]; then - missing_verity+=(--allow-missing-verity) -fi - # Build the UKI using bootc container ukify # This computes the composefs digest, reads kargs from kargs.d, and invokes ukify -bootc container ukify "${containerukifyargs[@]}" "${missing_verity[@]}" -- "${ukifyargs[@]}" +bootc container ukify "${containerukifyargs[@]}" "${kernel_params[@]}" "${missing_verity[@]}" -- "${ukifyargs[@]}" diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index 9a7c307ff..06845b9e6 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -4,7 +4,7 @@ use std::ffi::{CString, OsStr, OsString}; use std::fs::File; -use std::io::{BufWriter, Seek}; +use std::io::{BufWriter, Seek, SeekFrom}; use std::os::fd::AsFd; use std::os::unix::process::CommandExt; use std::process::Command; @@ -27,6 +27,7 @@ use composefs_boot::BootOps as _; use etc_merge::{compute_diff, print_diff}; use fn_error_context::context; use indoc::indoc; +use ocidir::cap_std::ambient_authority; use ostree::gio; use ostree_container::store::PrepareResult; use ostree_ext::container as ostree_container; @@ -415,6 +416,23 @@ pub(crate) enum ContainerOpts { /// Identifier for image; if not provided, the running image will be used. image: Option, }, + /// Split kernel and rootfs from a container image + /// + /// This command extracts the kernel (vmlinuz and initramfs.img) from the + /// container rootfs and moves them to a separate output directory, organized + /// by kernel version + /// + /// Example: + /// bootc container split-kernel-rootfs --rootfs /target-rootfs --output /out + SplitKernelAndRootfs { + /// Operate on the provided rootfs + #[clap(long, default_value = "/")] + rootfs: Utf8PathBuf, + + /// Output directory for the extracted kernel files + #[clap(long)] + output: Utf8PathBuf, + }, /// Build a Unified Kernel Image (UKI) using ukify. /// /// This command computes the necessary arguments from the container image @@ -626,6 +644,19 @@ pub(crate) enum FsverityOpts { }, } +#[derive(Debug, clap::Subcommand, PartialEq, Eq)] +pub(crate) enum UkiSubcommands { + /// Extract kernel + initrd from a UKI + /// The output (vmlinuz + initramfs.img) is placed in a directory named + /// after the kernel version found in the UKI + Extract { + /// The path to the UKI PE + path: Utf8PathBuf, + /// The output path + output_path: Utf8PathBuf, + }, +} + /// Hidden, internal only options #[derive(Debug, clap::Subcommand, PartialEq, Eq)] pub(crate) enum InternalsOpts { @@ -742,6 +773,9 @@ pub(crate) enum InternalsOpts { /// Block device inspection tools. #[clap(subcommand)] Blockdev(BlockdevOpts), + /// For various UKI operations + #[clap(subcommand)] + Uki(UkiSubcommands), } /// Subcommands for `bootc internals blockdev`. @@ -1837,6 +1871,41 @@ async fn run_from_opt(opt: Opt) -> Result<()> { )?; Ok(()) } + ContainerOpts::SplitKernelAndRootfs { rootfs, output } => { + use crate::kernel::{KernelType, find_kernel}; + + let root = Dir::open_ambient_dir(&rootfs, ambient_authority())?; + + let kernel_internal = find_kernel(&root)? + .ok_or_else(|| anyhow::anyhow!("No kernel found in rootfs"))?; + + if kernel_internal.kernel.unified { + anyhow::bail!("UKIs are not supported"); + } + + let kver = &kernel_internal.kernel.version; + let kernel_output_dir = output.join(kver); + std::fs::create_dir_all(&kernel_output_dir)?; + + match &kernel_internal.k_type { + KernelType::Vmlinuz { path, initramfs } => { + let vmlinuz_src = rootfs.join(path); + let initramfs_src = rootfs.join(initramfs); + let vmlinuz_dst = kernel_output_dir.join("vmlinuz"); + let initramfs_dst = kernel_output_dir.join("initramfs.img"); + + std::fs::rename(&vmlinuz_src, &vmlinuz_dst).context("Moving vmlinuz")?; + std::fs::rename(&initramfs_src, &initramfs_dst) + .context("Moving initramfs")?; + } + + KernelType::Uki { .. } => { + unreachable!("UKIs should have been rejected above"); + } + } + + Ok(()) + } ContainerOpts::ComputeComposefsDigest { path, write_dumpfile_to, @@ -2319,6 +2388,40 @@ async fn run_from_opt(opt: Opt) -> Result<()> { println!(); Ok(()) } + InternalsOpts::Uki(uki_opts) => match uki_opts { + UkiSubcommands::Extract { path, output_path } => { + let mut uki_file = + std::fs::File::open(&path).with_context(|| format!("Opening {path}"))?; + + let uname = + composefs_boot::uki::get_text_section_buffered(&mut uki_file, ".uname") + .context("Getting uname")?; + + std::fs::create_dir_all(&output_path).context("Creating output directory")?; + + let output_dir = Dir::open_ambient_dir(&output_path, ambient_authority()) + .context("Opening output dir")?; + output_dir.create_dir(&uname)?; + + let output_dir = output_dir.open_dir(&uname)?; + + for (section_name, file_name) in + [(".linux", "vmlinuz"), (".initrd", "initramfs.img")] + { + uki_file + .seek(SeekFrom::Start(0)) + .context("Seeking to start")?; + let section = + composefs_boot::uki::get_section_buffered(&mut uki_file, section_name) + .with_context(|| format!("Getting {section_name} section"))?; + output_dir + .write(file_name, section) + .with_context(|| format!("Writing {file_name}"))?; + } + + Ok(()) + } + }, }, Opt::State(opts) => match opts { StateOpts::WipeOstree => { diff --git a/crates/tests-integration/src/container.rs b/crates/tests-integration/src/container.rs index a7d8e89d6..14396107b 100644 --- a/crates/tests-integration/src/container.rs +++ b/crates/tests-integration/src/container.rs @@ -1,8 +1,10 @@ +use cap_std_ext::cap_std::ambient_authority; +use cap_std_ext::cap_std::fs::Dir; use indoc::indoc; use scopeguard::defer; use serde::Deserialize; -use std::fs; use std::process::Command; +use std::{fs, path::Path}; use anyhow::{Context, Result}; use camino::Utf8Path; @@ -72,6 +74,24 @@ pub(crate) fn test_bootc_container_inspect() -> Result<()> { ); // Version should be non-empty after stripping extension assert!(!version.is_empty(), "version should not be empty for UKI"); + + let target_root = Dir::open_ambient_dir(TARGET, ambient_authority()) + .with_context(|| format!("{TARGET} not found"))?; + + // For UKI make sure vmlinuz + initrd don't exist + let usr_lib_mod = Path::new("usr/lib/modules").join(version); + assert!( + target_root.exists(&usr_lib_mod), + "'{usr_lib_mod:?}' does not exist" + ); + assert!( + !target_root.exists(usr_lib_mod.join("vmlinuz")), + "vmlinuz should not exist for UKI" + ); + assert!( + !target_root.exists(usr_lib_mod.join("initramfs.img")), + "initramfs should not exist for UKI" + ); } o => eprintln!("notice: Unhandled variant for kernel check: {o:?}"), } @@ -188,6 +208,8 @@ fn test_variant_base_crosscheck() -> Result<()> { Ok(()) } +const TARGET: &str = "/run/target"; + /// Verify exported tar has correct size/mode/content vs source. /// Checks all critical paths (kernel, boot) plus ~10% random sample. pub(crate) fn test_container_export_tar() -> Result<()> { @@ -195,7 +217,6 @@ pub(crate) fn test_container_export_tar() -> Result<()> { use std::io::Read; use std::os::unix::fs::MetadataExt; - const TARGET: &str = "/run/target"; const CRITICAL: &[&str] = &["usr/lib/modules/", "usr/lib/ostree-boot/", "boot/"]; anyhow::ensure!( diff --git a/docs/src/man/bootc-container-split-kernel-and-rootfs.8.md b/docs/src/man/bootc-container-split-kernel-and-rootfs.8.md new file mode 100644 index 000000000..61c2a85f5 --- /dev/null +++ b/docs/src/man/bootc-container-split-kernel-and-rootfs.8.md @@ -0,0 +1,38 @@ +# NAME + +bootc-container-split-kernel-and-rootfs - Split kernel and rootfs from a container image + +# SYNOPSIS + +bootc container split-kernel-and-rootfs + +# DESCRIPTION + +Split kernel and rootfs from a container image + +# OPTIONS + + +**--rootfs**=*ROOTFS* + + Operate on the provided rootfs + + Default: / + +**--output**=*OUTPUT* + + Output directory for the extracted kernel files + + + +# EXAMPLES + +TODO: Add practical examples showing how to use this command. + +# SEE ALSO + +**bootc**(8) + +# VERSION + + diff --git a/docs/src/man/bootc-container.8.md b/docs/src/man/bootc-container.8.md index b8b540972..1b1fce62f 100644 --- a/docs/src/man/bootc-container.8.md +++ b/docs/src/man/bootc-container.8.md @@ -21,6 +21,7 @@ Operations which can be executed as part of a container build |---------|-------------| | **bootc container inspect** | Output information about the container image | | **bootc container lint** | Perform relatively inexpensive static analysis checks as part of a container build | +| **bootc container split-kernel-and-rootfs** | Split kernel and rootfs from a container image | | **bootc container ukify** | Build a Unified Kernel Image (UKI) using ukify | diff --git a/tmt/tests/Dockerfile.upgrade b/tmt/tests/Dockerfile.upgrade index 561e2e0a7..b66393114 100644 --- a/tmt/tests/Dockerfile.upgrade +++ b/tmt/tests/Dockerfile.upgrade @@ -13,6 +13,16 @@ ARG filesystem=ext4 FROM scratch AS packaging COPY contrib/packaging / +# Get kernel + initrd from the UKI +FROM localhost/bootc as kernel +ARG boot_type +RUN <<-EOF + if test "${boot_type}" = "uki"; then + kver=$(bootc container inspect --rootfs / --json | jq -r '.kernel.version') + bootc internals uki extract /boot/EFI/Linux/$kver.efi /boot + fi +EOF + # Create the upgrade content (a simple marker file). # For UKI builds, we also remove the existing UKI so that seal-uki can # regenerate it with the correct composefs digest for this derived image. @@ -36,16 +46,26 @@ RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp --mount=type=secret,id=secureboot_key \ --mount=type=secret,id=secureboot_cert \ --mount=type=bind,from=packaging,src=/,target=/run/packaging \ + --mount=type=bind,from=kernel,src=/,target=/run/kernel \ --mount=type=bind,from=upgrade-base,src=/,target=/run/target <> /tmp/Containerfile.drop-lbis <<-EOF - FROM base as base-final - RUN rm -rf /boot/EFI/Linux/*.efi + allow_missing_verity=() - FROM base as sealed-uki - RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ - --mount=type=bind,from=base-final,src=/,target=/run/target \ - /usr/bin/seal-uki /run/target /out /run/secrets $allow_missing_verity $seal_state + if [[ "$(bootc status --json | jq -r '.status.booted.composefs.missingVerityAllowed')" == "true" ]]; then + allow_missing_verity=("--allow-missing-verity") + fi - FROM base-final + seal_state="unsealed" - # Copy the sealed UKI and finalize the image remove raw kernel, create symlinks - RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ - --mount=type=bind,from=sealed-uki,src=/,target=/run/sealed-uki \ - /usr/bin/finalize-uki /run/sealed-uki/out + cat >> /tmp/Containerfile.drop-lbis <<-EOF +FROM base as kernel +RUN <<-RUNEOF + kver=\$(bootc container inspect --rootfs / --json | jq -r '.kernel.version') + bootc internals uki extract /boot/EFI/Linux/\$kver.efi /boot +RUNEOF + +FROM base as base-final +RUN rm -rf /boot/EFI/Linux/*.efi + +FROM base as sealed-uki +RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ + --mount=type=bind,from=kernel,src=/,target=/run/kernel \ + --mount=type=bind,from=base-final,src=/,target=/run/target <