diff --git a/Dockerfile b/Dockerfile index 31f515800..00a05d7cf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -83,7 +83,7 @@ RUN --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ # Install systemd-ukify and systemd-boot for UKIs # This also installs systemd-boot for the grub UKI case which is not ideal... if [[ "${boot_type}" == "uki" ]]; then - pkgs_to_install+=(systemd-ukify) + pkgs_to_install+=(systemd-ukify binutils) fi if [[ ${#pkgs_to_install[@]} -gt 0 ]]; then @@ -135,7 +135,10 @@ ARG pkgversion ARG SOURCE_DATE_EPOCH ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH} # Build RPM directly from source, using cached target directory -RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome RPM_VERSION="${pkgversion}" /src/contrib/packaging/build-rpm +RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ + --mount=type=cache,target=/src/target \ + --mount=type=cache,target=/var/roothome \ + RPM_VERSION="${pkgversion}" /src/contrib/packaging/build-rpm # Build a systemd-sysext containing just the bootc binary. # Skips RPM machinery entirely for fast incremental rebuilds. @@ -218,11 +221,10 @@ COPY --from=update-generated-from-code /src/docs/src/*.schema.json /docs/src/ # ---- # Perform all filesystem transformations except generating the sealed UKI (if configured) -FROM base as base-penultimate +FROM base as base-source ARG variant ARG bootloader ARG boot_type -ARG baseconfigs="" # Switch to a signed systemd-boot, if configured RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ @@ -247,6 +249,12 @@ rm -rf /var/cache rm -rf /run/rhsm EORUN + +FROM base-source as base-penultimate-source +ARG boot_type +ARG variant +ARG baseconfigs="" + # Configure the rootfs ARG rootfs="" RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ @@ -265,8 +273,23 @@ RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp COPY --from=packaging /usr-extras/ /usr/ # Clean up package manager caches RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ - --mount=type=bind,from=packaging,src=/,target=/run/packaging \ + --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..c493aeeb4 100755 --- a/contrib/packaging/seal-uki +++ b/contrib/packaging/seal-uki @@ -2,32 +2,82 @@ # 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 + ;; + + # The kernel version + "--kver") + kver="$2" + shift + shift + ;; + + # Path to the kernel + "--kernel") + kernel="$2" + shift + shift + ;; + + # Path to the initrd + "--initramfs") + initramfs="$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 || -z $initramfs || -z $kver ]]; then + echo "kernel, initramfs and kver are required" >&2 + exit 1 fi +kernel_params=(--kernel "$kernel" --initramfs "$initramfs" --kver "$kver") + mkdir -p "${output}" # Baseline ukify options @@ -45,12 +95,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/bootc_composefs/status.rs b/crates/lib/src/bootc_composefs/status.rs index ac1b261dd..49a6349c8 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -36,7 +36,8 @@ use bootc_utils::try_deserialize_timestamp; use cap_std_ext::{cap_std::fs::Dir, dirext::CapStdExtDirExt}; use ostree_container::OstreeImageReference; use ostree_ext::container::{self as ostree_container}; -use ostree_ext::containers_image_proxy; +use ostree_ext::containers_image_proxy::{ImageProxy, ImageReference}; + use ostree_ext::oci_spec; use ostree_ext::{container::deploy::ORIGIN_CONTAINER, oci_spec::image::ImageConfiguration}; @@ -379,14 +380,16 @@ pub(crate) fn list_bootloader_entries(storage: &Storage) -> Result Result { let mut config = crate::deploy::new_proxy_config(); - ostree_ext::container::merge_default_container_proxy_opts(&mut config)?; - let proxy = containers_image_proxy::ImageProxy::new_with_config(config).await?; + + ostree_ext::container::apply_container_proxy_opts_for_transport(&mut config, imgref.transport)?; + + let proxy = ImageProxy::new_with_config(config).await?; let img = proxy - .open_image(&imgref) + .open_image_ref(&imgref) .await .with_context(|| format!("Opening image {imgref}"))?; diff --git a/crates/lib/src/bootc_composefs/update.rs b/crates/lib/src/bootc_composefs/update.rs index a1fb2722f..79c823159 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -58,7 +58,7 @@ pub(crate) async fn is_image_pulled( imgref: &ImageReference, ) -> Result<(Option, ImgConfigManifest)> { let imgref_repr = imgref.to_image_proxy_ref()?; - let img_config_manifest = get_container_manifest_and_config(&imgref_repr.to_string()).await?; + let img_config_manifest = get_container_manifest_and_config(&imgref_repr).await?; let img_digest = img_config_manifest.manifest.config().digest().digest(); diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index dd80fd6ad..9275db060 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -442,6 +442,19 @@ pub(crate) enum ContainerOpts { #[clap(long)] write_dumpfile_to: Option, + /// The kernel version. + /// Required if kernel is passed + #[clap(long, requires = "kernel")] + kver: Option, + + /// Path to the kernel + #[clap(long, requires = "initramfs", requires = "kver")] + kernel: Option, + + /// Path to the initramfs + #[clap(long, requires = "kernel")] + initramfs: Option, + /// Additional arguments to pass to ukify (after `--`). #[clap(last = true)] args: Vec, @@ -1902,12 +1915,37 @@ async fn run_from_opt(opt: Opt) -> Result<()> { kargs, allow_missing_verity, write_dumpfile_to, + kernel, + kver, + initramfs, args, } => { + let kernel = match (kernel, initramfs) { + (Some(path), Some(initramfs)) => Some(crate::kernel::KernelInternal { + kernel: crate::kernel::Kernel { + unified: false, + version: kver + .ok_or_else(|| anyhow::anyhow!("Expected kver to be present"))?, + }, + k_type: crate::kernel::KernelType::Vmlinuz { path, initramfs }, + }), + + (None, None) => { + eprintln!( + "Warning: `bootc container ukify` without --kernel and --initramfs parameters is deprecated and will be removed in a future version" + ); + None + } + + // Shouldn't happen due to clap constraints but for sanity + _ => anyhow::bail!("--kernel and --initramfs must be provided together"), + }; + crate::ukify::build_ukify( &rootfs, &kargs, &args, + kernel, allow_missing_verity, write_dumpfile_to.as_deref(), ) diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index c9fcaf88c..d49c6a350 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -2017,8 +2017,7 @@ async fn install_to_filesystem_impl( // Pre-flight disk space check for native composefs install path. { let imgref = &state.source.imageref; - let imgref_repr = imgref.to_string(); - let img_manifest_config = get_container_manifest_and_config(&imgref_repr).await?; + let img_manifest_config = get_container_manifest_and_config(&imgref).await?; crate::store::ensure_composefs_dir(&rootfs.physical_root)?; // Use init_path since the repo may not exist yet during install let (cfs_repo, _created) = crate::store::ComposefsRepository::init_path( diff --git a/crates/lib/src/ukify.rs b/crates/lib/src/ukify.rs index c36cdb360..06008e7ef 100644 --- a/crates/lib/src/ukify.rs +++ b/crates/lib/src/ukify.rs @@ -15,6 +15,7 @@ use fn_error_context::context; use crate::bootc_composefs::digest::compute_composefs_digest; use crate::bootc_composefs::status::ComposefsCmdline; +use crate::kernel::KernelInternal; /// Build a UKI from the given rootfs. /// @@ -30,6 +31,7 @@ pub(crate) async fn build_ukify( rootfs: &Utf8Path, extra_kargs: &[String], args: &[OsString], + kernel: Option, allow_missing_fsverity: bool, write_dumpfile_to: Option<&Utf8Path>, ) -> Result<()> { @@ -52,12 +54,14 @@ pub(crate) async fn build_ukify( 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}"))?; + let kernel_final = match kernel { + Some(ref kernel) => kernel, + None => &crate::kernel::find_kernel(&root)? + .ok_or_else(|| anyhow::anyhow!("No kernel found in {rootfs}"))?, + }; // Extract vmlinuz and initramfs paths, or bail if this is already a UKI - let (vmlinuz_path, initramfs_path) = match kernel.k_type { + let (vmlinuz_path, initramfs_path) = match &kernel_final.k_type { crate::kernel::KernelType::Vmlinuz { path, initramfs } => (path, initramfs), crate::kernel::KernelType::Uki { path, .. } => { anyhow::bail!("Cannot build UKI: rootfs already contains a UKI at {path}"); @@ -65,17 +69,31 @@ pub(crate) async fn build_ukify( }; // 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}"); + // + // NOTE: Not using cap_std here as the vmlinuz/initramfs path from + // args can be outside of "rootfs" + if kernel.is_some() { + if !vmlinuz_path.exists() { + anyhow::bail!("Kernel not found at {vmlinuz_path}"); + } + + if !initramfs_path.exists() { + anyhow::bail!("Initramfs not found at {initramfs_path}"); + } + } else { + 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 @@ -105,7 +123,7 @@ pub(crate) async fn build_ukify( .arg("--initrd") .arg(&initramfs_path) .arg("--uname") - .arg(&kernel.kernel.version) + .arg(&kernel_final.kernel.version) .arg("--cmdline") .arg(&cmdline_str) .arg("--os-release") @@ -132,7 +150,7 @@ mod tests { let tempdir = tempfile::tempdir().unwrap(); let path = Utf8Path::from_path(tempdir.path()).unwrap(); - let result = build_ukify(path, &[], &[], false, None).await; + let result = build_ukify(path, &[], &[], None, false, None).await; assert!(result.is_err()); let err = format!("{:#}", result.unwrap_err()); assert!( @@ -150,7 +168,7 @@ mod tests { 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, &[], &[], false, None).await; + let result = build_ukify(path, &[], &[], None, false, None).await; assert!(result.is_err()); let err = format!("{:#}", result.unwrap_err()); assert!( diff --git a/crates/ostree-ext/src/container/mod.rs b/crates/ostree-ext/src/container/mod.rs index 3419bdf82..e73361852 100644 --- a/crates/ostree-ext/src/container/mod.rs +++ b/crates/ostree-ext/src/container/mod.rs @@ -496,6 +496,20 @@ pub fn version_for_config(config: &oci_spec::image::ImageConfiguration) -> Optio None } +/// Apply appropriate container proxy options based on transport type +pub fn apply_container_proxy_opts_for_transport( + config: &mut containers_image_proxy::ImageProxyConfig, + transport: Transport, +) -> Result<()> { + if transport == Transport::ContainerStorage { + // Fetching from containers-storage, may require privileges to read files + merge_default_container_proxy_opts_with_isolation(config, None) + } else { + // Apply our defaults to the proxy config + merge_default_container_proxy_opts(config) + } +} + pub mod deploy; mod encapsulate; pub use encapsulate::*; diff --git a/crates/ostree-ext/src/container/store.rs b/crates/ostree-ext/src/container/store.rs index 8acf5831e..f810bb7ca 100644 --- a/crates/ostree-ext/src/container/store.rs +++ b/crates/ostree-ext/src/container/store.rs @@ -635,13 +635,7 @@ impl ImageImporter { imgref: &OstreeImageReference, mut config: ImageProxyConfig, ) -> Result { - if imgref.imgref.transport == Transport::ContainerStorage { - // Fetching from containers-storage, may require privileges to read files - merge_default_container_proxy_opts_with_isolation(&mut config, None)?; - } else { - // Apply our defaults to the proxy config - merge_default_container_proxy_opts(&mut config)?; - } + apply_container_proxy_opts_for_transport(&mut config, imgref.imgref.transport)?; let proxy = ImageProxy::new_with_config(config).await?; system_repo_journal_print( diff --git a/crates/tests-integration/src/container.rs b/crates/tests-integration/src/container.rs index a7d8e89d6..57dc3af1d 100644 --- a/crates/tests-integration/src/container.rs +++ b/crates/tests-integration/src/container.rs @@ -1,8 +1,8 @@ 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 +72,18 @@ 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"); + + // For UKI make sure vmlinuz + initrd don't exist + let usr_lib_mod = Path::new("/usr/lib/modules").join(version); + assert!(usr_lib_mod.exists(), "'{usr_lib_mod:?}' does not exist"); + assert!( + !usr_lib_mod.join("vmlinuz").exists(), + "vmlinuz should not exist for UKI" + ); + assert!( + !usr_lib_mod.join("initramfs.img").exists(), + "initramfs should not exist for UKI" + ); } o => eprintln!("notice: Unhandled variant for kernel check: {o:?}"), } diff --git a/docs/src/man/bootc-container-ukify.8.md b/docs/src/man/bootc-container-ukify.8.md index bad9e10cc..b77f37b0f 100644 --- a/docs/src/man/bootc-container-ukify.8.md +++ b/docs/src/man/bootc-container-ukify.8.md @@ -35,6 +35,18 @@ Any additional arguments after `--` are passed through to ukify unchanged. Write a dumpfile to this path +**--kver**=*KVER* + + The kernel version. Required if kernel is passed + +**--kernel**=*KERNEL* + + Path to the kernel + +**--initramfs**=*INITRAMFS* + + Path to the initramfs + # EXAMPLES diff --git a/tmt/plans/integration.fmf b/tmt/plans/integration.fmf index b3366648b..9830acd87 100644 --- a/tmt/plans/integration.fmf +++ b/tmt/plans/integration.fmf @@ -217,6 +217,7 @@ execute: how: fmf test: - /tmt/tests/tests/test-37-install-no-boot-dir + extra-fixme_skip_if_composefs: true /plan-37-upgrade-check-status: summary: Verify upgrade --check populates cached update in status @@ -232,6 +233,7 @@ execute: how: fmf test: - /tmt/tests/tests/test-38-install-bootloader-none + extra-fixme_skip_if_composefs: true /plan-39-upgrade-tag: summary: Test bootc upgrade --tag functionality with containers-storage diff --git a/tmt/tests/Dockerfile.upgrade b/tmt/tests/Dockerfile.upgrade index 561e2e0a7..02e50e3ac 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 + objcopy -O binary --only-section=.initrd /boot/EFI/Linux/*.efi /boot/initramfs.img + objcopy -O binary --only-section=.linux /boot/EFI/Linux/*.efi /boot/vmlinuz + 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,28 @@ 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 + objcopy -O binary --only-section=.initrd /boot/EFI/Linux/*.efi /boot/initramfs.img + objcopy -O binary --only-section=.linux /boot/EFI/Linux/*.efi /boot/vmlinuz +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 <