mirror of
https://github.com/containers/bootc.git
synced 2026-02-05 06:45:13 +01:00
feat: Add bootc container ukify command
Add a new subcommand that builds a Unified Kernel Image (UKI) by computing the necessary arguments from a container image and invoking ukify. This simplifies the sealed image build workflow by having bootc internally compute: - The composefs digest (via existing compute-composefs-digest logic) - Kernel arguments from /usr/lib/bootc/kargs.d/*.toml files - Paths to kernel, initrd, and os-release Any additional arguments are passed through to ukify unchanged, allowing full control over signing, output paths, and other ukify options. The seal-uki script is updated to use this new command instead of manually computing these values and invoking ukify directly. Also adds kargs.d configuration files for the sealed UKI workflow: - 10-rootfs-rw.toml: Mount root filesystem read-write - 21-console-hvc0.toml: Console configuration for QEMU/virtio Closes: #1955 Assisted-by: OpenCode (Opus 4.5) Signed-off-by: John Eckersberg <jeckersb@redhat.com>
This commit is contained in:
committed by
Colin Walters
parent
4f51a5fbcb
commit
5d6dd67276
@@ -12,15 +12,7 @@ shift
|
||||
secrets=$1
|
||||
shift
|
||||
|
||||
# Compute the composefs digest from the target rootfs
|
||||
composefs_digest=$(bootc container compute-composefs-digest "${target}")
|
||||
|
||||
# Build the kernel command line
|
||||
# enforcing=0: https://github.com/bootc-dev/bootc/issues/1826
|
||||
# TODO: pick up kargs from /usr/lib/bootc/kargs.d
|
||||
cmdline="composefs=${composefs_digest} console=ttyS0,115200n8 console=hvc0 enforcing=0 rw"
|
||||
|
||||
# Find the kernel version
|
||||
# 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
|
||||
@@ -29,12 +21,14 @@ fi
|
||||
|
||||
mkdir -p "${output}"
|
||||
|
||||
ukify build \
|
||||
--linux "${target}/usr/lib/modules/${kver}/vmlinuz" \
|
||||
--initrd "${target}/usr/lib/modules/${kver}/initramfs.img" \
|
||||
--uname="${kver}" \
|
||||
--cmdline "${cmdline}" \
|
||||
--os-release "@${target}/usr/lib/os-release" \
|
||||
# Build the UKI using bootc container ukify
|
||||
# This computes the composefs digest, reads kargs from kargs.d, and invokes ukify
|
||||
#
|
||||
# WORKAROUND: SELinux must be permissive for sealed UKI boot
|
||||
# See https://github.com/bootc-dev/bootc/issues/1826
|
||||
bootc container ukify --rootfs "${target}" \
|
||||
--karg enforcing=0 \
|
||||
-- \
|
||||
--signtool sbsign \
|
||||
--secureboot-private-key "${secrets}/secureboot_key" \
|
||||
--secureboot-certificate "${secrets}/secureboot_cert" \
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# Mount the root filesystem read-write
|
||||
kargs = ["rw"]
|
||||
@@ -1,2 +1,3 @@
|
||||
# https://bugzilla.redhat.com/show_bug.cgi?id=2353887
|
||||
kargs = ["console=hvc0"]
|
||||
# console=ttyS0 for QEMU serial, console=hvc0 for virtio/Xen console
|
||||
kargs = ["console=ttyS0,115200n8", "console=hvc0"]
|
||||
|
||||
@@ -392,6 +392,29 @@ pub(crate) enum ContainerOpts {
|
||||
/// Identifier for image; if not provided, the running image will be used.
|
||||
image: Option<String>,
|
||||
},
|
||||
/// Build a Unified Kernel Image (UKI) using ukify.
|
||||
///
|
||||
/// This command computes the necessary arguments from the container image
|
||||
/// (kernel, initrd, cmdline, os-release) and invokes ukify with them.
|
||||
/// Any additional arguments after `--` are passed through to ukify unchanged.
|
||||
///
|
||||
/// Example:
|
||||
/// bootc container ukify --rootfs /target -- --output /output/uki.efi
|
||||
Ukify {
|
||||
/// Operate on the provided rootfs.
|
||||
#[clap(long, default_value = "/")]
|
||||
rootfs: Utf8PathBuf,
|
||||
|
||||
/// Additional kernel arguments to append to the cmdline.
|
||||
/// Can be specified multiple times.
|
||||
/// This is a temporary workaround and will be removed.
|
||||
#[clap(long = "karg", hide = true)]
|
||||
kargs: Vec<String>,
|
||||
|
||||
/// Additional arguments to pass to ukify (after `--`).
|
||||
#[clap(last = true)]
|
||||
args: Vec<OsString>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Subcommands which operate on images.
|
||||
@@ -1598,6 +1621,11 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
ContainerOpts::Ukify {
|
||||
rootfs,
|
||||
kargs,
|
||||
args,
|
||||
} => crate::ukify::build_ukify(&rootfs, &kargs, &args),
|
||||
},
|
||||
Opt::Completion { shell } => {
|
||||
use clap_complete::aot::generate;
|
||||
|
||||
@@ -1587,7 +1587,7 @@ async fn prepare_install(
|
||||
|
||||
let composefs_required = if let Some(root) = target_rootfs.as_ref() {
|
||||
crate::kernel::find_kernel(root)?
|
||||
.map(|k| k.unified)
|
||||
.map(|k| k.kernel.unified)
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use camino::Utf8PathBuf;
|
||||
use cap_std_ext::cap_std::fs::Dir;
|
||||
use cap_std_ext::dirext::CapStdExtDirExt;
|
||||
use serde::Serialize;
|
||||
@@ -25,6 +26,28 @@ pub(crate) struct Kernel {
|
||||
pub(crate) unified: bool,
|
||||
}
|
||||
|
||||
/// Internal-only kernel wrapper with extra information (paths to
|
||||
/// vmlinuz, initramfs) that are useful but we don't want to leak out
|
||||
/// via serialization to inspection.
|
||||
///
|
||||
/// `Kernel` implements `From<KernelInternal>` so we can just `.into()`
|
||||
/// to get the "public" form where needed.
|
||||
pub(crate) struct KernelInternal {
|
||||
pub(crate) kernel: Kernel,
|
||||
/// Path to vmlinuz for traditional kernels.
|
||||
/// This is `None` for UKI images.
|
||||
pub(crate) vmlinuz: Option<Utf8PathBuf>,
|
||||
/// Path to initramfs.img for traditional kernels.
|
||||
/// This is `None` for UKI images.
|
||||
pub(crate) initramfs: Option<Utf8PathBuf>,
|
||||
}
|
||||
|
||||
impl From<KernelInternal> for Kernel {
|
||||
fn from(kernel_internal: KernelInternal) -> Self {
|
||||
kernel_internal.kernel
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the kernel in a container image root directory.
|
||||
///
|
||||
/// This function first attempts to find a UKI in `/boot/EFI/Linux/*.efi`.
|
||||
@@ -32,28 +55,38 @@ pub(crate) struct Kernel {
|
||||
/// layout with `/usr/lib/modules/<version>/vmlinuz`.
|
||||
///
|
||||
/// Returns `None` if no kernel is found.
|
||||
pub(crate) fn find_kernel(root: &Dir) -> Result<Option<Kernel>> {
|
||||
pub(crate) fn find_kernel(root: &Dir) -> Result<Option<KernelInternal>> {
|
||||
// First, try to find a UKI
|
||||
if let Some(uki_filename) = find_uki_filename(root)? {
|
||||
let version = uki_filename
|
||||
.strip_suffix(".efi")
|
||||
.unwrap_or(&uki_filename)
|
||||
.to_owned();
|
||||
return Ok(Some(Kernel {
|
||||
version,
|
||||
unified: true,
|
||||
return Ok(Some(KernelInternal {
|
||||
kernel: Kernel {
|
||||
version,
|
||||
unified: true,
|
||||
},
|
||||
vmlinuz: None,
|
||||
initramfs: None,
|
||||
}));
|
||||
}
|
||||
|
||||
// Fall back to checking for a traditional kernel via ostree_ext
|
||||
if let Some(kernel_dir) = ostree_ext::bootabletree::find_kernel_dir_fs(root)? {
|
||||
let version = kernel_dir
|
||||
if let Some(modules_dir) = ostree_ext::bootabletree::find_kernel_dir_fs(root)? {
|
||||
let version = modules_dir
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow::anyhow!("kernel dir should have a file name: {kernel_dir}"))?
|
||||
.ok_or_else(|| anyhow::anyhow!("kernel dir should have a file name: {modules_dir}"))?
|
||||
.to_owned();
|
||||
return Ok(Some(Kernel {
|
||||
version,
|
||||
unified: false,
|
||||
let vmlinuz = modules_dir.join("vmlinuz");
|
||||
let initramfs = modules_dir.join("initramfs.img");
|
||||
return Ok(Some(KernelInternal {
|
||||
kernel: Kernel {
|
||||
version,
|
||||
unified: false,
|
||||
},
|
||||
vmlinuz: Some(vmlinuz),
|
||||
initramfs: Some(initramfs),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -93,6 +126,7 @@ fn find_uki_filename(root: &Dir) -> Result<Option<String>> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use camino::Utf8Path;
|
||||
use cap_std_ext::{cap_std, cap_tempfile, dirext::CapStdExtDirExt};
|
||||
|
||||
#[test]
|
||||
@@ -111,9 +145,21 @@ mod tests {
|
||||
b"fake kernel",
|
||||
)?;
|
||||
|
||||
let kernel = find_kernel(&tempdir)?.expect("should find kernel");
|
||||
assert_eq!(kernel.version, "6.12.0-100.fc41.x86_64");
|
||||
assert!(!kernel.unified);
|
||||
let kernel_internal = find_kernel(&tempdir)?.expect("should find kernel");
|
||||
assert_eq!(kernel_internal.kernel.version, "6.12.0-100.fc41.x86_64");
|
||||
assert!(!kernel_internal.kernel.unified);
|
||||
assert_eq!(
|
||||
kernel_internal.vmlinuz.as_deref(),
|
||||
Some(Utf8Path::new(
|
||||
"usr/lib/modules/6.12.0-100.fc41.x86_64/vmlinuz"
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
kernel_internal.initramfs.as_deref(),
|
||||
Some(Utf8Path::new(
|
||||
"usr/lib/modules/6.12.0-100.fc41.x86_64/initramfs.img"
|
||||
))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -123,9 +169,11 @@ mod tests {
|
||||
tempdir.create_dir_all("boot/EFI/Linux")?;
|
||||
tempdir.atomic_write("boot/EFI/Linux/fedora-6.12.0.efi", b"fake uki")?;
|
||||
|
||||
let kernel = find_kernel(&tempdir)?.expect("should find kernel");
|
||||
assert_eq!(kernel.version, "fedora-6.12.0");
|
||||
assert!(kernel.unified);
|
||||
let kernel_internal = find_kernel(&tempdir)?.expect("should find kernel");
|
||||
assert_eq!(kernel_internal.kernel.version, "fedora-6.12.0");
|
||||
assert!(kernel_internal.kernel.unified);
|
||||
assert!(kernel_internal.vmlinuz.is_none());
|
||||
assert!(kernel_internal.initramfs.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -141,10 +189,10 @@ mod tests {
|
||||
tempdir.create_dir_all("boot/EFI/Linux")?;
|
||||
tempdir.atomic_write("boot/EFI/Linux/fedora-6.12.0.efi", b"fake uki")?;
|
||||
|
||||
let kernel = find_kernel(&tempdir)?.expect("should find kernel");
|
||||
let kernel_internal = find_kernel(&tempdir)?.expect("should find kernel");
|
||||
// UKI should take precedence
|
||||
assert_eq!(kernel.version, "fedora-6.12.0");
|
||||
assert!(kernel.unified);
|
||||
assert_eq!(kernel_internal.kernel.version, "fedora-6.12.0");
|
||||
assert!(kernel_internal.kernel.unified);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ pub mod spec;
|
||||
mod status;
|
||||
mod store;
|
||||
mod task;
|
||||
mod ukify;
|
||||
mod utils;
|
||||
|
||||
#[cfg(feature = "docgen")]
|
||||
|
||||
@@ -863,7 +863,7 @@ pub(crate) fn container_inspect(
|
||||
)?;
|
||||
let kargs = crate::bootc_kargs::get_kargs_in_root(&root, std::env::consts::ARCH)?;
|
||||
let kargs: Vec<String> = kargs.iter_str().map(|s| s.to_owned()).collect();
|
||||
let kernel = crate::kernel::find_kernel(&root)?;
|
||||
let kernel = crate::kernel::find_kernel(&root)?.map(Into::into);
|
||||
let inspect = crate::spec::ContainerInspect { kargs, kernel };
|
||||
|
||||
// Determine output format: explicit --format wins, then --json, then default to human-readable
|
||||
|
||||
166
crates/lib/src/ukify.rs
Normal file
166
crates/lib/src/ukify.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
//! Build Unified Kernel Images (UKI) using ukify.
|
||||
//!
|
||||
//! This module provides functionality to build UKIs by computing the necessary
|
||||
//! arguments from a container image and invoking the ukify tool.
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use bootc_kernel_cmdline::utf8::Cmdline;
|
||||
use bootc_utils::CommandRunExt;
|
||||
use camino::Utf8Path;
|
||||
use cap_std_ext::cap_std::fs::Dir;
|
||||
use fn_error_context::context;
|
||||
|
||||
use crate::bootc_composefs::digest::compute_composefs_digest;
|
||||
use crate::composefs_consts::COMPOSEFS_CMDLINE;
|
||||
|
||||
/// Build a UKI from the given rootfs.
|
||||
///
|
||||
/// This function:
|
||||
/// 1. Verifies that ukify is available
|
||||
/// 2. Finds the kernel in the rootfs
|
||||
/// 3. Computes the composefs digest
|
||||
/// 4. Reads kernel arguments from kargs.d
|
||||
/// 5. Appends any additional kargs provided via --karg
|
||||
/// 6. Invokes ukify with computed arguments plus any pass-through args
|
||||
#[context("Building UKI")]
|
||||
pub(crate) fn build_ukify(
|
||||
rootfs: &Utf8Path,
|
||||
extra_kargs: &[String],
|
||||
args: &[OsString],
|
||||
) -> Result<()> {
|
||||
// Warn if --karg is used (temporary workaround)
|
||||
if !extra_kargs.is_empty() {
|
||||
tracing::warn!(
|
||||
"The --karg flag is temporary and will be removed as soon as possible \
|
||||
(https://github.com/bootc-dev/bootc/issues/1826)"
|
||||
);
|
||||
}
|
||||
|
||||
// Verify ukify is available
|
||||
if !crate::utils::have_executable("ukify")? {
|
||||
anyhow::bail!(
|
||||
"ukify executable not found in PATH. Please install systemd-ukify or equivalent."
|
||||
);
|
||||
}
|
||||
|
||||
// Open the rootfs directory
|
||||
let root = Dir::open_ambient_dir(rootfs, cap_std_ext::cap_std::ambient_authority())
|
||||
.with_context(|| format!("Opening rootfs {rootfs}"))?;
|
||||
|
||||
// Find the kernel
|
||||
let kernel = crate::kernel::find_kernel(&root)?
|
||||
.ok_or_else(|| anyhow::anyhow!("No kernel found in {rootfs}"))?;
|
||||
|
||||
// We can only build a UKI from a traditional kernel, not from an existing UKI
|
||||
if kernel.kernel.unified {
|
||||
anyhow::bail!(
|
||||
"Cannot build UKI: rootfs already contains a UKI at boot/EFI/Linux/{}.efi",
|
||||
kernel.kernel.version
|
||||
);
|
||||
}
|
||||
|
||||
// Get paths from the kernel info
|
||||
let vmlinuz_path = kernel
|
||||
.vmlinuz
|
||||
.ok_or_else(|| anyhow::anyhow!("Traditional kernel should have vmlinuz path"))?;
|
||||
let initramfs_path = kernel
|
||||
.initramfs
|
||||
.ok_or_else(|| anyhow::anyhow!("Traditional kernel should have initramfs path"))?;
|
||||
|
||||
// Verify kernel and initramfs exist
|
||||
if !root
|
||||
.try_exists(&vmlinuz_path)
|
||||
.context("Checking for vmlinuz")?
|
||||
{
|
||||
anyhow::bail!("Kernel not found at {vmlinuz_path}");
|
||||
}
|
||||
if !root
|
||||
.try_exists(&initramfs_path)
|
||||
.context("Checking for initramfs")?
|
||||
{
|
||||
anyhow::bail!("Initramfs not found at {initramfs_path}");
|
||||
}
|
||||
|
||||
// Compute the composefs digest
|
||||
let composefs_digest = compute_composefs_digest(rootfs, None)?;
|
||||
|
||||
// Get kernel arguments from kargs.d
|
||||
let mut cmdline = crate::bootc_kargs::get_kargs_in_root(&root, std::env::consts::ARCH)?;
|
||||
|
||||
// Add the composefs digest
|
||||
let composefs_param = format!("{COMPOSEFS_CMDLINE}={composefs_digest}");
|
||||
cmdline.extend(&Cmdline::from(composefs_param));
|
||||
|
||||
// Add any extra kargs provided via --karg
|
||||
for karg in extra_kargs {
|
||||
cmdline.extend(&Cmdline::from(karg));
|
||||
}
|
||||
|
||||
let cmdline_str = cmdline.to_string();
|
||||
|
||||
// Build the ukify command with cwd set to rootfs so paths can be relative
|
||||
let mut cmd = Command::new("ukify");
|
||||
cmd.current_dir(rootfs);
|
||||
cmd.arg("build")
|
||||
.arg("--linux")
|
||||
.arg(&vmlinuz_path)
|
||||
.arg("--initrd")
|
||||
.arg(&initramfs_path)
|
||||
.arg("--uname")
|
||||
.arg(&kernel.kernel.version)
|
||||
.arg("--cmdline")
|
||||
.arg(&cmdline_str)
|
||||
.arg("--os-release")
|
||||
.arg("@usr/lib/os-release");
|
||||
|
||||
// Add pass-through arguments
|
||||
cmd.args(args);
|
||||
|
||||
tracing::debug!("Executing ukify: {:?}", cmd);
|
||||
|
||||
// Run ukify
|
||||
cmd.run_inherited().context("Running ukify")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn test_build_ukify_no_kernel() {
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
let path = Utf8Path::from_path(tempdir.path()).unwrap();
|
||||
|
||||
let result = build_ukify(path, &[], &[]);
|
||||
assert!(result.is_err());
|
||||
let err = format!("{:#}", result.unwrap_err());
|
||||
assert!(
|
||||
err.contains("No kernel found") || err.contains("ukify executable not found"),
|
||||
"Unexpected error message: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_ukify_already_uki() {
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
let path = Utf8Path::from_path(tempdir.path()).unwrap();
|
||||
|
||||
// Create a UKI structure
|
||||
fs::create_dir_all(tempdir.path().join("boot/EFI/Linux")).unwrap();
|
||||
fs::write(tempdir.path().join("boot/EFI/Linux/test.efi"), b"fake uki").unwrap();
|
||||
|
||||
let result = build_ukify(path, &[], &[]);
|
||||
assert!(result.is_err());
|
||||
let err = format!("{:#}", result.unwrap_err());
|
||||
assert!(
|
||||
err.contains("already contains a UKI") || err.contains("ukify executable not found"),
|
||||
"Unexpected error message: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,6 @@ pub(crate) fn find_mount_option<'a>(
|
||||
.next()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn have_executable(name: &str) -> Result<bool> {
|
||||
let Some(path) = std::env::var_os("PATH") else {
|
||||
return Ok(false);
|
||||
|
||||
42
docs/src/man/bootc-container-ukify.8.md
Normal file
42
docs/src/man/bootc-container-ukify.8.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# NAME
|
||||
|
||||
bootc-container-ukify - Build a Unified Kernel Image (UKI) using ukify
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
bootc container ukify [OPTIONS] [-- UKIFY_ARGS...]
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
Build a Unified Kernel Image (UKI) using ukify
|
||||
|
||||
This command computes the necessary arguments from the container image
|
||||
(kernel, initrd, cmdline, os-release) and invokes ukify with them.
|
||||
Any additional arguments after `--` are passed through to ukify unchanged.
|
||||
|
||||
# OPTIONS
|
||||
|
||||
<!-- BEGIN GENERATED OPTIONS -->
|
||||
**ARGS**
|
||||
|
||||
Additional arguments to pass to ukify (after `--`)
|
||||
|
||||
**--rootfs**=*ROOTFS*
|
||||
|
||||
Operate on the provided rootfs
|
||||
|
||||
Default: /
|
||||
|
||||
<!-- END GENERATED OPTIONS -->
|
||||
|
||||
# EXAMPLES
|
||||
|
||||
bootc container ukify --rootfs /target -- --output /output/uki.efi
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
**bootc**(8), **ukify**(1)
|
||||
|
||||
# VERSION
|
||||
|
||||
<!-- VERSION PLACEHOLDER -->
|
||||
@@ -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 ukify** | Build a Unified Kernel Image (UKI) using ukify |
|
||||
|
||||
<!-- END GENERATED SUBCOMMANDS -->
|
||||
|
||||
|
||||
Reference in New Issue
Block a user