diff --git a/.github/actions/bootc-ubuntu-setup/action.yml b/.github/actions/bootc-ubuntu-setup/action.yml index b9bdf9174..8d759c91a 100644 --- a/.github/actions/bootc-ubuntu-setup/action.yml +++ b/.github/actions/bootc-ubuntu-setup/action.yml @@ -1,5 +1,10 @@ name: 'Bootc Ubuntu Setup' description: 'Default host setup' +inputs: + libvirt: + description: 'Install libvirt and virtualization stack' + required: false + default: 'false' runs: using: 'composite' steps: @@ -45,3 +50,10 @@ runs: id: set_arch shell: bash run: echo "ARCH=$(arch)" >> $GITHUB_ENV + # Install libvirt stack if requested + - name: Install libvirt and virtualization stack + if: ${{ inputs.libvirt == 'true' }} + shell: bash + run: | + set -eux + sudo apt install -y libkrb5-dev pkg-config libvirt-dev genisoimage qemu-utils qemu-kvm qemu-utils libvirt-daemon-system diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac05e2c57..1cb7843a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -131,8 +131,8 @@ jobs: - uses: actions/checkout@v4 - name: Bootc Ubuntu Setup uses: ./.github/actions/bootc-ubuntu-setup - - name: Install qemu-utils - run: sudo apt install -y qemu-utils + with: + libvirt: true - name: Build container and disk image run: | @@ -163,12 +163,10 @@ jobs: - uses: actions/checkout@v4 - name: Bootc Ubuntu Setup uses: ./.github/actions/bootc-ubuntu-setup - - name: Install deps - run: | - sudo apt-get update - # see https://tmt.readthedocs.io/en/stable/overview.html#install - sudo apt install -y libkrb5-dev pkg-config libvirt-dev genisoimage qemu-kvm qemu-utils libvirt-daemon-system just - pip install --user "tmt[provision-virtual]" + with: + libvirt: true + - name: Install tmt + run: pip install --user "tmt[provision-virtual]" - name: Create folder to save disk image run: mkdir -p target @@ -192,3 +190,29 @@ jobs: with: name: tmt-log-PR-${{ github.event.number }}-${{ matrix.test_os }}-${{ env.ARCH }}-${{ matrix.tmt_plan }} path: /var/tmp/tmt + # This variant does composefs testing + test-integration-cfs: + strategy: + fail-fast: false + matrix: + test_os: [centos-10] + + runs-on: ubuntu-24.04 + + steps: + - uses: actions/checkout@v4 + - name: Bootc Ubuntu Setup + uses: ./.github/actions/bootc-ubuntu-setup + with: + libvirt: true + + - name: Build container and disk image + run: | + sudo just build-sealed-integration-test-disk + + - name: Archive disk image + uses: actions/upload-artifact@v4 + with: + name: PR-${{ github.event.number }}-${{ matrix.test_os }}-${{ env.ARCH }}-sealed-disk + path: target/bootc-integration-test.qcow2 + retention-days: 1 diff --git a/Cargo.lock b/Cargo.lock index 83dc5064e..c46767309 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -487,9 +487,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" diff --git a/Dockerfile.cfsuki b/Dockerfile.cfsuki new file mode 100644 index 000000000..dddaf84b3 --- /dev/null +++ b/Dockerfile.cfsuki @@ -0,0 +1,65 @@ +# Override via --build-arg=base= to use a different base +ARG base=localhost/bootc +# This is where we get the tools to build the UKI +ARG buildroot=quay.io/fedora/fedora:42 +FROM $base AS base + +FROM $buildroot as buildroot-base +RUN < Result { /// * insecure - Whether fsverity is optional or not #[context("Mounting composefs image")] pub fn mount_composefs_image(sysroot: &OwnedFd, name: &str, insecure: bool) -> Result { - let mut repo = Repository::::open_path(sysroot, "composefs")?; + let mut repo = Repository::::open_path(sysroot, "composefs")?; repo.set_insecure(insecure); repo.mount(name).context("Failed to mount composefs image") } @@ -282,7 +282,7 @@ pub fn setup_root(args: Args) -> Result<()> { // TODO: Deduplicate this with composefs branch karg parser None => &std::fs::read_to_string("/proc/cmdline")?, }; - let (image, insecure) = get_cmdline_composefs::(cmdline)?; + let (image, insecure) = get_cmdline_composefs::(cmdline)?; let new_root = match args.root_fs { Some(path) => open_root_fs(&path).context("Failed to clone specified root fs")?, diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index 682e949e7..1a26fd992 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -73,7 +73,7 @@ similar-asserts = { workspace = true } static_assertions = { workspace = true } [features] -default = ["install-to-disk"] +default = ["install-to-disk", "composefs-backend"] # This feature enables `bootc install to-disk`, which is considered just a "demo" # or reference installer; we expect most nontrivial use cases to be using # `bootc install to-filesystem`. diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index 4dae7d5cb..b62fdfd8d 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -14,14 +14,11 @@ use cap_std_ext::{ }; use clap::ValueEnum; use composefs::fs::read_file; -use composefs::tree::{FileSystem, RegularFile}; +use composefs::tree::RegularFile; use composefs_boot::bootloader::{PEType, EFI_ADDON_DIR_EXT, EFI_ADDON_FILE_EXT, EFI_EXT}; use composefs_boot::BootOps; use fn_error_context::context; -use ostree_ext::composefs::{ - fsverity::{FsVerityHashValue, Sha256HashValue}, - repository::Repository as ComposefsRepository, -}; +use ostree_ext::composefs::fsverity::{FsVerityHashValue, Sha512HashValue}; use ostree_ext::composefs_boot::bootloader::UsrLibModulesVmlinuz; use ostree_ext::composefs_boot::{ bootloader::BootEntry as ComposefsBootEntry, cmdline::get_cmdline_composefs, @@ -32,7 +29,6 @@ use rustix::path::Arg; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::bootc_composefs::repo::open_composefs_repo; use crate::bootc_composefs::state::{get_booted_bls, write_composefs_state}; use crate::bootc_composefs::status::get_sorted_uki_boot_entries; use crate::composefs_consts::{TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED}; @@ -40,6 +36,7 @@ use crate::parsers::bls_config::{BLSConfig, BLSConfigType}; use crate::parsers::grub_menuconfig::MenuEntry; use crate::spec::ImageReference; use crate::task::Task; +use crate::{bootc_composefs::repo::open_composefs_repo, store::ComposefsFilesystem}; use crate::{ composefs_consts::{ BOOT_LOADER_ENTRIES, COMPOSEFS_CMDLINE, ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_DIGEST, @@ -54,7 +51,7 @@ use crate::install::{RootSetup, State}; /// Contains the EFP's filesystem UUID. Used by grub pub(crate) const EFI_UUID_FILE: &str = "efiuuid.cfg"; /// The EFI Linux directory -const EFI_LINUX: &str = "EFI/Linux"; +pub(crate) const EFI_LINUX: &str = "EFI/Linux"; /// Timeout for systemd-boot bootloader menu const SYSTEMD_TIMEOUT: &str = "timeout 5"; @@ -68,9 +65,9 @@ const SYSTEMD_UKI_DIR: &str = "EFI/Linux/bootc"; pub(crate) enum BootSetupType<'a> { /// For initial setup, i.e. install to-disk - Setup((&'a RootSetup, &'a State, &'a FileSystem)), + Setup((&'a RootSetup, &'a State, &'a ComposefsFilesystem)), /// For `bootc upgrade` - Upgrade((&'a FileSystem, &'a Host)), + Upgrade((&'a ComposefsFilesystem, &'a Host)), } #[derive( @@ -107,8 +104,8 @@ impl TryFrom<&str> for BootType { } } -impl From<&ComposefsBootEntry> for BootType { - fn from(entry: &ComposefsBootEntry) -> Self { +impl From<&ComposefsBootEntry> for BootType { + fn from(entry: &ComposefsBootEntry) -> Self { match entry { ComposefsBootEntry::Type1(..) => Self::Bls, ComposefsBootEntry::Type2(..) => Self::Uki, @@ -129,6 +126,26 @@ fi ) } +/// Returns `true` if detect the target rootfs carries a UKI. +pub(crate) fn container_root_has_uki(root: &Dir) -> Result { + let Some(boot) = root.open_dir_optional(crate::install::BOOT)? else { + return Ok(false); + }; + let Some(efi_linux) = boot.open_dir_optional(EFI_LINUX)? else { + return Ok(false); + }; + for entry in efi_linux.entries()? { + let entry = entry?; + let name = entry.file_name(); + let name = Path::new(&name); + let extension = name.extension().and_then(|v| v.to_str()); + if extension == Some("efi") { + return Ok(true); + } + } + Ok(false) +} + pub fn get_esp_partition(device: &str) -> Result<(String, Option)> { let device_info = bootc_blockdev::partitions_of(Utf8Path::new(device))?; let esp = device_info @@ -164,8 +181,8 @@ pub fn type1_entry_conf_file_name(sort_key: impl std::fmt::Display) -> String { /// * repo - The composefs repository #[context("Computing boot digest")] fn compute_boot_digest( - entry: &UsrLibModulesVmlinuz, - repo: &ComposefsRepository, + entry: &UsrLibModulesVmlinuz, + repo: &crate::store::ComposefsRepository, ) -> Result { let vmlinuz = read_file(&entry.vmlinuz, &repo).context("Reading vmlinuz")?; @@ -238,9 +255,9 @@ fn find_vmlinuz_initrd_duplicates(digest: &str) -> Result> { #[context("Writing BLS entries to disk")] fn write_bls_boot_entries_to_disk( boot_dir: &Utf8PathBuf, - deployment_id: &Sha256HashValue, - entry: &UsrLibModulesVmlinuz, - repo: &ComposefsRepository, + deployment_id: &Sha512HashValue, + entry: &UsrLibModulesVmlinuz, + repo: &crate::store::ComposefsRepository, ) -> Result<()> { let id_hex = deployment_id.to_hex(); @@ -283,8 +300,8 @@ fn write_bls_boot_entries_to_disk( /// # Returns /// - (title, version) fn osrel_title_and_version( - fs: &FileSystem, - repo: &ComposefsRepository, + fs: &crate::store::ComposefsFilesystem, + repo: &crate::store::ComposefsRepository, ) -> Result, Option)>> { // Every update should have its own /usr/lib/os-release let (dir, fname) = fs @@ -342,9 +359,9 @@ struct BLSEntryPath<'a> { pub(crate) fn setup_composefs_bls_boot( setup_type: BootSetupType, // TODO: Make this generic - repo: ComposefsRepository, - id: &Sha256HashValue, - entry: &ComposefsBootEntry, + repo: crate::store::ComposefsRepository, + id: &Sha512HashValue, + entry: &ComposefsBootEntry, ) -> Result { let id_hex = id.to_hex(); @@ -552,8 +569,8 @@ pub(crate) fn setup_composefs_bls_boot( /// Writes a PortableExecutable to ESP along with any PE specific or Global addons #[context("Writing {file_path} to ESP")] fn write_pe_to_esp( - repo: &ComposefsRepository, - file: &RegularFile, + repo: &crate::store::ComposefsRepository, + file: &RegularFile, file_path: &Utf8Path, pe_type: PEType, uki_id: &String, @@ -571,7 +588,7 @@ fn write_pe_to_esp( let cmdline = uki::get_cmdline(&efi_bin).context("Getting UKI cmdline")?; let (composefs_cmdline, insecure) = - get_cmdline_composefs::(cmdline).context("Parsing composefs=")?; + get_cmdline_composefs::(cmdline).context("Parsing composefs=")?; // If the UKI cmdline does not match what the user has passed as cmdline option // NOTE: This will only be checked for new installs and now upgrades/switches @@ -659,7 +676,7 @@ fn write_grub_uki_menuentry( root_path: Utf8PathBuf, setup_type: &BootSetupType, boot_label: String, - id: &Sha256HashValue, + id: &Sha512HashValue, esp_device: &String, ) -> Result<()> { let boot_dir = root_path.join("boot"); @@ -747,7 +764,7 @@ fn write_systemd_uki_config( esp_dir: &Dir, setup_type: &BootSetupType, boot_label: String, - id: &Sha256HashValue, + id: &Sha512HashValue, ) -> Result<()> { let default_sort_key = "0"; @@ -816,9 +833,9 @@ fn write_systemd_uki_config( pub(crate) fn setup_composefs_uki_boot( setup_type: BootSetupType, // TODO: Make this generic - repo: ComposefsRepository, - id: &Sha256HashValue, - entries: Vec>, + repo: crate::store::ComposefsRepository, + id: &Sha512HashValue, + entries: Vec>, ) -> Result<()> { let (root_path, esp_device, bootloader, is_insecure_from_opts, uki_addons) = match setup_type { BootSetupType::Setup((root_setup, state, ..)) => { @@ -1004,3 +1021,34 @@ pub(crate) fn setup_composefs_boot( Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use cap_std_ext::cap_std; + + #[test] + fn test_root_has_uki() -> Result<()> { + // Test case 1: No boot directory + let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?; + assert_eq!(container_root_has_uki(&tempdir)?, false); + + // Test case 2: boot directory exists but no EFI/Linux + tempdir.create_dir(crate::install::BOOT)?; + assert_eq!(container_root_has_uki(&tempdir)?, false); + + // Test case 3: boot/EFI/Linux exists but no .efi files + tempdir.create_dir_all("boot/EFI/Linux")?; + assert_eq!(container_root_has_uki(&tempdir)?, false); + + // Test case 4: boot/EFI/Linux exists with non-.efi file + tempdir.atomic_write("boot/EFI/Linux/readme.txt", b"some file")?; + assert_eq!(container_root_has_uki(&tempdir)?, false); + + // Test case 5: boot/EFI/Linux exists with .efi file + tempdir.atomic_write("boot/EFI/Linux/bootx64.efi", b"fake efi binary")?; + assert_eq!(container_root_has_uki(&tempdir)?, true); + + Ok(()) + } +} diff --git a/crates/lib/src/bootc_composefs/repo.rs b/crates/lib/src/bootc_composefs/repo.rs index 51323c716..8cd5506c3 100644 --- a/crates/lib/src/bootc_composefs/repo.rs +++ b/crates/lib/src/bootc_composefs/repo.rs @@ -4,9 +4,7 @@ use std::sync::Arc; use anyhow::{Context, Result}; use ostree_ext::composefs::{ - fsverity::{FsVerityHashValue, Sha256HashValue}, - repository::Repository as ComposefsRepository, - tree::FileSystem, + fsverity::{FsVerityHashValue, Sha512HashValue}, util::Sha256Digest, }; use ostree_ext::composefs_boot::{bootloader::BootEntry as ComposefsBootEntry, BootOps}; @@ -20,10 +18,8 @@ use cap_std_ext::cap_std::{ambient_authority, fs::Dir}; use crate::install::{RootSetup, State}; -pub(crate) fn open_composefs_repo( - rootfs_dir: &Dir, -) -> Result> { - ComposefsRepository::open_path(rootfs_dir, "composefs") +pub(crate) fn open_composefs_repo(rootfs_dir: &Dir) -> Result { + crate::store::ComposefsRepository::open_path(rootfs_dir, "composefs") .context("Failed to open composefs repository") } @@ -78,10 +74,10 @@ pub(crate) async fn pull_composefs_repo( transport: &String, image: &String, ) -> Result<( - ComposefsRepository, - Vec>, - Sha256HashValue, - FileSystem, + crate::store::ComposefsRepository, + Vec>, + Sha512HashValue, + crate::store::ComposefsFilesystem, )> { let rootfs_dir = Dir::open_ambient_dir("/sysroot", ambient_authority())?; @@ -98,8 +94,9 @@ pub(crate) async fn pull_composefs_repo( tracing::info!("id: {}, verity: {}", hex::encode(id), verity.to_hex()); let repo = open_composefs_repo(&rootfs_dir)?; - let mut fs = create_composefs_filesystem(&repo, &hex::encode(id), None) - .context("Failed to create composefs filesystem")?; + let mut fs: crate::store::ComposefsFilesystem = + create_composefs_filesystem(&repo, &hex::encode(id), None) + .context("Failed to create composefs filesystem")?; let entries = fs.transform_for_boot(&repo)?; let id = fs.commit_image(&repo, None)?; diff --git a/crates/lib/src/bootc_composefs/state.rs b/crates/lib/src/bootc_composefs/state.rs index b163d3717..e7b8ce70b 100644 --- a/crates/lib/src/bootc_composefs/state.rs +++ b/crates/lib/src/bootc_composefs/state.rs @@ -10,7 +10,7 @@ use camino::Utf8PathBuf; use cap_std_ext::cap_std::ambient_authority; use cap_std_ext::cap_std::fs::Dir; use cap_std_ext::dirext::CapStdExtDirExt; -use composefs::fsverity::{FsVerityHashValue, Sha256HashValue}; +use composefs::fsverity::{FsVerityHashValue, Sha512HashValue}; use fn_error_context::context; use ostree_ext::container::deploy::ORIGIN_CONTAINER; @@ -107,7 +107,7 @@ pub(crate) fn copy_etc_to_state( #[context("Writing composefs state")] pub(crate) fn write_composefs_state( root_path: &Utf8PathBuf, - deployment_id: Sha256HashValue, + deployment_id: Sha512HashValue, imgref: &ImageReference, staged: bool, boot_type: BootType, diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index 3412a6b03..f3737b37a 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -6,13 +6,15 @@ use std::ffi::{CString, OsStr, OsString}; use std::io::Seek; use std::os::unix::process::CommandExt; use std::process::Command; +use std::sync::Arc; use anyhow::{ensure, Context, Result}; -use camino::Utf8PathBuf; +use camino::{Utf8Path, Utf8PathBuf}; use cap_std_ext::cap_std; use cap_std_ext::cap_std::fs::Dir; use clap::Parser; use clap::ValueEnum; +use composefs_boot::BootOps as _; use etc_merge::{compute_diff, print_diff}; use fn_error_context::context; use indoc::indoc; @@ -23,11 +25,13 @@ use ostree_ext::composefs::fsverity::FsVerityHashValue; use ostree_ext::composefs::splitstream::SplitStreamWriter; use ostree_ext::container as ostree_container; use ostree_ext::container_utils::ostree_booted; +use ostree_ext::containers_image_proxy::ImageProxyConfig; use ostree_ext::keyfileext::KeyFileExt; use ostree_ext::ostree; use ostree_ext::sysroot::SysrootLock; use schemars::schema_for; use serde::{Deserialize, Serialize}; +use tempfile::tempdir_in; #[cfg(feature = "composefs-backend")] use crate::bootc_composefs::{ @@ -40,9 +44,11 @@ use crate::bootc_composefs::{ }; use crate::deploy::RequiredHostSpec; use crate::lints; +use crate::podstorage::set_additional_image_store; use crate::progress_jsonl::{ProgressWriter, RawProgressFd}; use crate::spec::Host; use crate::spec::ImageReference; +use crate::store::ComposefsRepository; use crate::utils::sigpolicy_from_opt; /// Shared progress options @@ -315,6 +321,12 @@ pub(crate) enum ContainerOpts { #[clap(long)] no_truncate: bool, }, + /// Output the bootable composefs digest. + #[clap(hide = true)] + ComputeComposefsDigest { + /// Identifier for image; if not provided, the running image will be used. + image: Option, + }, } /// Subcommands which operate on images. @@ -1335,6 +1347,55 @@ async fn run_from_opt(opt: Opt) -> Result<()> { )?; Ok(()) } + ContainerOpts::ComputeComposefsDigest { image } => { + // Allocate a tempdir + let td = tempdir_in("/var/tmp")?; + let td = td.path(); + let td = &Dir::open_ambient_dir(td, cap_std::ambient_authority())?; + + td.create_dir("repo")?; + let repo = td.open_dir("repo")?; + let mut repo = + ComposefsRepository::open_path(&repo, ".").context("Init cfs repo")?; + // We don't need to hard require verity on the *host* system, we're just computing a checksum here + repo.set_insecure(true); + let repo = &Arc::new(repo); + + let mut proxycfg = ImageProxyConfig::default(); + + let image = if let Some(image) = image { + image + } else { + let host_container_store = Utf8Path::new("/run/host-container-storage"); + // If no image is provided, assume that we're running in a container in privileged mode + // with access to the container storage. + let container_info = crate::containerenv::get_container_execution_info(&root)?; + let iid = container_info.imageid; + tracing::debug!("Computing digest of {iid}"); + + if !host_container_store.try_exists()? { + anyhow::bail!("Must be readonly mount of host container store: {host_container_store}"); + } + // And ensure we're finding the image in the host storage + let mut cmd = Command::new("skopeo"); + set_additional_image_store(&mut cmd, "/run/host-container-storage"); + proxycfg.skopeo_cmd = Some(cmd); + iid + }; + + let imgref = format!("containers-storage:{image}"); + let (imgid, verity) = composefs_oci::pull(repo, &imgref, None, Some(proxycfg)) + .await + .context("Pulling image")?; + let imgid = hex::encode(imgid); + let mut fs = composefs_oci::image::create_filesystem(repo, &imgid, Some(&verity)) + .context("Populating fs")?; + fs.transform_for_boot(&repo).context("Preparing for boot")?; + let id = fs.compute_image_id(); + println!("{}", id.to_hex()); + + Ok(()) + } }, Opt::Image(opts) => match opts { ImageOpts::List { diff --git a/crates/lib/src/generator.rs b/crates/lib/src/generator.rs index 9f28e3a3b..a2e75318b 100644 --- a/crates/lib/src/generator.rs +++ b/crates/lib/src/generator.rs @@ -139,7 +139,6 @@ ExecStart=bootc internals fixup-etc-fstab\n\ #[cfg(test)] mod tests { use camino::Utf8Path; - use cap_std_ext::cmdext::CapStdExtCommandExt as _; use super::*; diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index defa9d19c..0ce5b6bee 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -70,7 +70,7 @@ use bootc_mount::Filesystem; use composefs::fsverity::FsVerityHashValue; /// The toplevel boot directory -const BOOT: &str = "boot"; +pub(crate) const BOOT: &str = "boot"; /// Directory for transient runtime state #[cfg(feature = "install-to-disk")] const RUN_BOOTC: &str = "/run/bootc"; @@ -247,7 +247,7 @@ pub(crate) struct InstallConfigOpts { pub(crate) stateroot: Option, } -#[derive(Debug, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Default, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)] pub(crate) struct InstallComposefsOpts { #[clap(long, default_value_t)] #[serde(default)] @@ -438,6 +438,10 @@ pub(crate) struct State { pub(crate) container_root: Dir, pub(crate) tempdir: TempDir, + /// Set if we have determined that composefs is required + #[allow(dead_code)] + pub(crate) composefs_required: bool, + // If Some, then --composefs_native is passed #[cfg(feature = "composefs-backend")] pub(crate) composefs_options: Option, @@ -793,6 +797,7 @@ async fn install_container( let sepolicy = sepolicy.as_ref(); let stateroot = state.stateroot(); + // TODO factor out this let (src_imageref, proxy_cfg) = if !state.source.in_host_mountns { (state.source.imageref.clone(), None) } else { @@ -1221,12 +1226,20 @@ async fn verify_target_fetch( Ok(()) } +fn root_has_uki(root: &Dir) -> Result { + #[cfg(feature = "composefs-backend")] + return crate::bootc_composefs::boot::container_root_has_uki(root); + + #[cfg(not(feature = "composefs-backend"))] + Ok(false) +} + /// Preparation for an install; validates and prepares some (thereafter immutable) global state. async fn prepare_install( config_opts: InstallConfigOpts, source_opts: InstallSourceOpts, target_opts: InstallTargetOpts, - _composefs_opts: Option, + composefs_options: Option, ) -> Result> { tracing::trace!("Preparing install"); let rootfs = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority()) @@ -1234,7 +1247,7 @@ async fn prepare_install( let host_is_container = crate::containerenv::is_container(&rootfs); let external_source = source_opts.source_imgref.is_some(); - let source = match source_opts.source_imgref { + let (source, target_rootfs) = match source_opts.source_imgref { None => { ensure!(host_is_container, "Either --source-imgref must be defined or this command must be executed inside a podman container."); @@ -1259,11 +1272,13 @@ async fn prepare_install( }; tracing::trace!("Read container engine info {:?}", container_info); - SourceInfo::from_container(&rootfs, &container_info)? + let source = SourceInfo::from_container(&rootfs, &container_info)?; + (source, Some(rootfs.try_clone()?)) } Some(source) => { crate::cli::require_root(false)?; - SourceInfo::from_imageref(&source, &rootfs)? + let source = SourceInfo::from_imageref(&source, &rootfs)?; + (source, None) } }; @@ -1291,6 +1306,15 @@ async fn prepare_install( }; tracing::debug!("Target image reference: {target_imgref}"); + let composefs_required = if let Some(root) = target_rootfs.as_ref() { + root_has_uki(root)? + } else { + false + }; + tracing::debug!("Composefs required: {composefs_required}"); + let composefs_options = + composefs_options.or_else(|| composefs_required.then_some(InstallComposefsOpts::default())); + // We need to access devices that are set up by the host udev bootc_mount::ensure_mirrored_host_mount("/dev")?; // We need to read our own container image (and any logically bound images) @@ -1371,8 +1395,9 @@ async fn prepare_install( container_root: rootfs, tempdir, host_is_container, + composefs_required, #[cfg(feature = "composefs-backend")] - composefs_options: _composefs_opts, + composefs_options, }); Ok(state) diff --git a/crates/lib/src/lints.rs b/crates/lib/src/lints.rs index 0e4a0416a..55b4f1978 100644 --- a/crates/lib/src/lints.rs +++ b/crates/lib/src/lints.rs @@ -27,6 +27,9 @@ use linkme::distributed_slice; use ostree_ext::ostree_prepareroot; use serde::Serialize; +#[cfg(feature = "composefs-backend")] +use crate::bootc_composefs::boot::EFI_LINUX; + /// Reference to embedded default baseimage content that should exist. const BASEIMAGE_REF: &str = "usr/share/doc/bootc/baseimage/base"; // https://systemd.io/API_FILE_SYSTEMS/ with /var added for us @@ -758,14 +761,27 @@ fn check_boot(root: &Dir, config: &LintExecutionConfig) -> LintResult { }; // First collect all entries to determine if the directory is empty - let entries: Result, _> = d.entries()?.collect(); - let entries = entries?; + let entries: Result, _> = d + .entries()? + .into_iter() + .map(|v| { + let v = v?; + anyhow::Ok(v.file_name()) + }) + .collect(); + let mut entries = entries?; + #[cfg(feature = "composefs-backend")] + { + // Work around https://github.com/containers/composefs-rs/issues/131 + let efidir = Utf8Path::new(EFI_LINUX) + .parent() + .map(|b| b.as_std_path()) + .unwrap(); + entries.remove(efidir.as_os_str()); + } if entries.is_empty() { return lint_ok(); } - // Gather sorted filenames - let mut entries = entries.iter().map(|v| v.file_name()).collect::>(); - entries.sort(); let header = "Found non-empty /boot"; let items = entries.iter().map(PathQuotedDisplay::new); @@ -973,6 +989,12 @@ mod tests { let root = &passing_fixture()?; let config = &LintExecutionConfig::default(); check_boot(&root, config).unwrap().unwrap(); + + // Verify creating EFI doesn't error + root.create_dir_all("EFI/Linux")?; + root.write("EFI/Linux/foo.efi", b"some dummy efi")?; + check_boot(&root, config).unwrap().unwrap(); + root.create_dir("boot/somesubdir")?; let Err(e) = check_boot(&root, config).unwrap() else { unreachable!() diff --git a/crates/lib/src/podstorage.rs b/crates/lib/src/podstorage.rs index 6ac8b0a5b..eaff56572 100644 --- a/crates/lib/src/podstorage.rs +++ b/crates/lib/src/podstorage.rs @@ -127,6 +127,17 @@ fn new_podman_cmd_in(storage_root: &Dir, run_root: &Dir) -> Result { Ok(cmd) } +/// Adjust the provided command (skopeo or podman e.g.) to reference +/// the provided path as an additional image store. +pub fn set_additional_image_store<'c>( + cmd: &'c mut Command, + ais: impl AsRef, +) -> &'c mut Command { + let ais = ais.as_ref(); + let storage_opt = format!("additionalimagestore={ais}"); + cmd.env("STORAGE_OPTS", storage_opt) +} + /// Ensure that "podman" is the first thing to touch the global storage /// instance. This is a workaround for https://github.com/bootc-dev/bootc/pull/1101#issuecomment-2653862974 /// Basically podman has special upgrade logic for when it is the first thing diff --git a/crates/lib/src/store/mod.rs b/crates/lib/src/store/mod.rs index cab4167e2..7dfbafb2a 100644 --- a/crates/lib/src/store/mod.rs +++ b/crates/lib/src/store/mod.rs @@ -37,6 +37,8 @@ use crate::utils::deployment_fd; /// See https://github.com/containers/composefs-rs/issues/159 pub type ComposefsRepository = composefs::repository::Repository; +#[cfg(feature = "composefs-backend")] +pub type ComposefsFilesystem = composefs::tree::FileSystem; /// Path to the physical root pub const SYSROOT: &str = "sysroot"; diff --git a/crates/xtask/src/xtask.rs b/crates/xtask/src/xtask.rs index 5f04395d6..e5281b5aa 100644 --- a/crates/xtask/src/xtask.rs +++ b/crates/xtask/src/xtask.rs @@ -11,6 +11,7 @@ use std::process::Command; use anyhow::{Context, Result}; use camino::{Utf8Path, Utf8PathBuf}; use fn_error_context::context; +use serde::Deserialize; use xshell::{cmd, Shell}; mod man; @@ -237,6 +238,14 @@ fn spec(sh: &Shell) -> Result<()> { Ok(()) } +#[derive(Debug, Deserialize)] +#[allow(dead_code)] +#[serde(rename_all = "PascalCase")] +struct ImageInspect { + pub id: String, + pub digest: String, +} + fn impl_srpm(sh: &Shell) -> Result { { let _g = sh.push_dir("target"); diff --git a/docs/src/man/bootc-install-to-disk.8.md b/docs/src/man/bootc-install-to-disk.8.md index 2c92959d8..ede4a86ed 100644 --- a/docs/src/man/bootc-install-to-disk.8.md +++ b/docs/src/man/bootc-install-to-disk.8.md @@ -114,6 +114,26 @@ more complex such as RAID, LVM, LUKS etc. Instead of targeting a block device, write to a file via loopback +**--composefs-native** + + + +**--insecure** + + + + Default: false + +**--bootloader**=*BOOTLOADER* + + + + Default: grub + +**--uki-addon**=*UKI_ADDON* + + Name of the UKI addons to install without the ".efi.addon" suffix. This option can be provided multiple times if multiple addons are to be installed + # EXAMPLES diff --git a/docs/src/man/bootc.8.md b/docs/src/man/bootc.8.md index adb1c72c8..3c22aacb4 100644 --- a/docs/src/man/bootc.8.md +++ b/docs/src/man/bootc.8.md @@ -33,6 +33,8 @@ pulled and `bootc upgrade`. | **bootc usr-overlay** | Add a transient writable overlayfs on `/usr` | | **bootc install** | Install the running container to a target | | **bootc container** | Operations which can be executed as part of a container build | +| **bootc composefs-finalize-staged** | | +| **bootc config-diff** | Diff current /etc configuration versus default | diff --git a/tests/build-sealed b/tests/build-sealed new file mode 100755 index 000000000..c69214f76 --- /dev/null +++ b/tests/build-sealed @@ -0,0 +1,46 @@ +#!/bin/bash +set -euo pipefail +# This should turn into https://github.com/bootc-dev/bootc/issues/1498 + +# The un-sealed container image we want to use +input_image=$1 +shift +# The output container image +output_image=$1 +shift +# Optional directory with secure boot keys; if none are provided, then we'll +# generate some under target/ +secureboot=${1:-} + +runv() { + set +x + "$@" +} + +graphroot=$(podman system info -f '{{.Store.GraphRoot}}') +echo "Computing composefs digest..." +cfs_digest=$(podman run --rm --privileged --read-only --security-opt=label=disable -v /sys:/sys:ro --net=none \ + -v ${graphroot}:/run/host-container-storage:ro --tmpfs /var "$input_image" bootc container compute-composefs-digest) + +if test -z "${secureboot}"; then + secureboot=$(pwd)/target/test-secureboot + mkdir -p ${secureboot} + cd $secureboot + if test '!' -f db.cer; then + echo "Generating test Secure Boot keys" + uuidgen --random > GUID.txt + openssl req -quiet -newkey rsa:4096 -nodes -keyout PK.key -new -x509 -sha256 -days 3650 -subj '/CN=Test Platform Key/' -out PK.crt + openssl x509 -outform DER -in PK.crt -out PK.cer + openssl req -quiet -newkey rsa:4096 -nodes -keyout KEK.key -new -x509 -sha256 -days 3650 -subj '/CN=Test Key Exchange Key/' -out KEK.crt + openssl x509 -outform DER -in KEK.crt -out KEK.cer + openssl req -quiet -newkey rsa:4096 -nodes -keyout db.key -new -x509 -sha256 -days 3650 -subj '/CN=Test Signature Database key/' -out db.crt + openssl x509 -outform DER -in db.crt -out db.cer + else + echo "Reusing Secure Boot keys in ${secureboot}" + fi + cd - +fi + +runv podman build -t $output_image --build-arg=COMPOSEFS_FSVERITY=${cfs_digest} --build-arg=base=${input_image} \ + --secret=id=key,src=${secureboot}/db.key \ + --secret=id=cert,src=${secureboot}/db.crt -f Dockerfile.cfsuki .