diff --git a/Dockerfile b/Dockerfile index a36bc5638..ea876c5ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -348,6 +348,9 @@ fi # target-base), and so without --bootloader too, which bcvk only takes with it. if [[ "${variant}" == composefs* ]]; then printf '[install]\nbootloader = "%s"\n' "${bootloader}" > /usr/lib/bootc/install/80-composefs-bootloader.toml + # The composefs backend doesn't install logically bound images yet: + # https://github.com/bootc-dev/bootc/issues/2540 + rm -vf /usr/lib/bootc/bound-images.d/* fi if [[ "${boot_type}" == "uki" ]]; then diff --git a/crates/lib/src/boundimage.rs b/crates/lib/src/boundimage.rs index 31b4e5fb3..c99aeb6fb 100644 --- a/crates/lib/src/boundimage.rs +++ b/crates/lib/src/boundimage.rs @@ -18,7 +18,7 @@ use crate::store::Storage; /// The path in a root for bound images; this directory should only contain /// symbolic links to `.container` or `.image` files. -const BOUND_IMAGE_DIR: &str = "usr/lib/bootc/bound-images.d"; +pub(crate) const BOUND_IMAGE_DIR: &str = "usr/lib/bootc/bound-images.d"; /// A subset of data parsed from a `.image` or `.container` file with /// the minimal information necessary to fetch the image. diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index 8f9f80fc4..bf589b91f 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -438,6 +438,31 @@ impl InstallComposefsOpts { } Ok(()) } + + /// The composefs backend doesn't install logically bound images yet + /// (), so fail rather + /// than install a system that is missing them. + pub(crate) fn validate_bound_images( + &self, + bound_images: BoundImagesOpt, + root: &Dir, + ) -> Result<()> { + if !self.composefs_backend || bound_images == BoundImagesOpt::Skip { + return Ok(()); + } + let images = crate::boundimage::query_bound_images(root)?; + if !images.is_empty() { + let images = images + .iter() + .map(|i| i.image.as_str()) + .collect::>() + .join(", "); + anyhow::bail!( + "Logically bound images are not supported with the composefs backend (found {images}); use --bound-images skip to install without them" + ); + } + Ok(()) + } } #[cfg(feature = "install-to-disk")] @@ -1778,6 +1803,8 @@ async fn prepare_install( composefs_options.composefs_backend |= composefs_required || composefs_default; composefs_options.validate(config_opts.bootloader.as_ref())?; + composefs_options.validate_bound_images(config_opts.bound_images, &rootfs)?; + // Read the file eagerly so we error out early, and before the mount changes // below hide a file bind mounted under e.g. /tmp. We may re-exec further down // and run this again with those mounts in place, so pass the content to the @@ -3168,6 +3195,57 @@ mod tests { assert_eq!(c.block_opts.device, "/dev/vda"); } + #[test] + fn test_composefs_opts_validate_bound_images() -> Result<()> { + use crate::boundimage::BOUND_IMAGE_DIR; + let unbound = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?; + unbound.create_dir_all(BOUND_IMAGE_DIR)?; + let bound = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?; + bound.create_dir_all("usr/share/containers/systemd")?; + bound.write( + "usr/share/containers/systemd/app.image", + "[Image]\nImage=quay.io/example/app:latest\n", + )?; + bound.create_dir_all(BOUND_IMAGE_DIR)?; + bound.symlink_contents( + "/usr/share/containers/systemd/app.image", + format!("{BOUND_IMAGE_DIR}/app.image"), + )?; + + // (composefs_backend, bound_images, root binds an image, valid) + let cases = [ + (false, BoundImagesOpt::Stored, true, true), + (true, BoundImagesOpt::Stored, false, true), + (true, BoundImagesOpt::Stored, true, false), + (true, BoundImagesOpt::Pull, true, false), + (true, BoundImagesOpt::Skip, true, true), + ]; + for (composefs_backend, bound_images, binds, valid) in cases { + let opts = InstallComposefsOpts { + composefs_backend, + ..Default::default() + }; + let root = if binds { &bound } else { &unbound }; + assert_eq!( + opts.validate_bound_images(bound_images, root).is_ok(), + valid, + "{opts:?} {bound_images:?} {binds}" + ); + } + + let opts = InstallComposefsOpts { + composefs_backend: true, + ..Default::default() + }; + assert_eq!( + opts.validate_bound_images(BoundImagesOpt::Stored, &bound) + .unwrap_err() + .to_string(), + "Logically bound images are not supported with the composefs backend (found quay.io/example/app:latest); use --bound-images skip to install without them" + ); + Ok(()) + } + #[test] fn source_fetch_reference_uses_config_id_for_host_images() { let cases = [