From e2f833f7643b1f3132294a752c3db4f9d5f00b03 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Tue, 23 Jul 2024 14:25:04 -0400 Subject: [PATCH] install: Fetch bound images from host's /var/lib/containers Instead of fetching images from the network at install time, require them to be present in the host's container storage (the same place we expect to find the bootc image). There are numerous advantages to this, such as the basic fact that this helps one avoid re-pulling images at each install. But more importantly it aligns with the needs of "hermetic builds" which want to pre-fetch content from the network into the target environment. For example when generating a disk image with bootc-image-builder or so, a build tool can consistently preload all the input images (including the bootc base) into the container store, and know that the install process to generate the disk will use those images. Signed-off-by: Colin Walters --- lib/src/boundimage.rs | 23 +++++++++++++- lib/src/install.rs | 74 ++++++++++++++++++++++++++++++++----------- 2 files changed, 78 insertions(+), 19 deletions(-) diff --git a/lib/src/boundimage.rs b/lib/src/boundimage.rs index a5bf06d19..9a0d82e68 100644 --- a/lib/src/boundimage.rs +++ b/lib/src/boundimage.rs @@ -11,6 +11,7 @@ use camino::Utf8Path; use cap_std_ext::cap_std::fs::Dir; use cap_std_ext::dirext::CapStdExtDirExt; use fn_error_context::context; +use ostree_ext::containers_image_proxy; use ostree_ext::ostree::Deployment; use ostree_ext::sysroot::SysrootLock; @@ -23,12 +24,18 @@ const BOUND_IMAGE_DIR: &str = "usr/lib/bootc/bound-images.d"; /// /// In the future this may be extended to include e.g. certificates or /// other pull options. -#[derive(PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq)] pub(crate) struct BoundImage { image: String, auth_file: Option, } +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct ResolvedBoundImage { + pub(crate) image: String, + pub(crate) digest: String, +} + /// Given a deployment, pull all container images it references. pub(crate) fn pull_bound_images(sysroot: &SysrootLock, deployment: &Deployment) -> Result<()> { let deployment_root = &crate::utils::deployment_fd(sysroot, deployment)?; @@ -86,6 +93,20 @@ pub(crate) fn query_bound_images(root: &Dir) -> Result> { Ok(bound_images) } +impl ResolvedBoundImage { + pub(crate) async fn from_image(src: &BoundImage) -> Result { + let proxy = containers_image_proxy::ImageProxy::new().await?; + let img = proxy + .open_image(&format!("containers-storage:{}", src.image)) + .await?; + let digest = proxy.fetch_manifest(&img).await?.0; + Ok(Self { + image: src.image.clone(), + digest, + }) + } +} + fn parse_image_file(file_contents: &tini::Ini) -> Result { let image: String = file_contents .get("Image", "Image") diff --git a/lib/src/install.rs b/lib/src/install.rs index 93403b750..66139ecd3 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -11,7 +11,7 @@ pub(crate) mod config; pub(crate) mod osconfig; use std::io::Write; -use std::os::fd::AsFd; +use std::os::fd::{AsFd, OwnedFd}; use std::os::unix::process::CommandExt; use std::path::Path; use std::process::Command; @@ -26,6 +26,7 @@ use camino::Utf8PathBuf; use cap_std::fs::{Dir, MetadataExt}; use cap_std_ext::cap_std; use cap_std_ext::cap_std::fs_utf8::DirEntry as DirEntryUtf8; +use cap_std_ext::cmdext::CapStdExtCommandExt; use cap_std_ext::prelude::CapStdExtDirExt; use chrono::prelude::*; use clap::ValueEnum; @@ -1271,6 +1272,7 @@ async fn install_with_sysroot( rootfs: &RootSetup, sysroot: &ostree::Sysroot, boot_uuid: &str, + bound_images: &[crate::boundimage::ResolvedBoundImage], ) -> Result<()> { let sysroot = SysrootLock::new_from_sysroot(&sysroot).await?; // And actually set up the container in that root, returning a deployment and @@ -1299,33 +1301,51 @@ async fn install_with_sysroot( tracing::debug!("Installed bootloader"); tracing::debug!("Perfoming post-deployment operations"); - let deployment_root = crate::utils::deployment_fd(&sysroot, &deployment)?; - let bound_images = if state.config_opts.skip_bound_images { - Vec::new() - } else { - crate::boundimage::query_bound_images(&deployment_root)? - }; if !bound_images.is_empty() { + // TODO: We shouldn't hardcode the overlay driver for source or + // target, but we currently need to in order to reference the location. + // For this one, containers-storage: is actually the *host*'s /var/lib/containers + // which we are accessing directly. + let storage_src = "containers-storage:"; // TODO: We only do this dance to initialize `/var` at install time if // there are bound images today; it minimizes side effects. // However going forward we really do need to handle a separate /var partition... // and to do that we may in the general case need to run the `var.mount` // target from the new root. + // Probably the best fix is for us to switch bound images to use the bootc storage. let varpath = format!("ostree/deploy/{stateroot}/var"); let var = rootfs .rootfs_fd .open_dir(&varpath) .with_context(|| format!("Opening {varpath}"))?; - Task::new("Mounting deployment /var", "mount") - .args(["--bind", ".", "/var"]) - .cwd(&var)? - .run()?; - // podman needs this - Task::new("Initializing /var/tmp", "systemd-tmpfiles") - .args(["--create", "--boot", "--prefix=/var/tmp"]) - .verbose() - .run()?; - crate::boundimage::pull_images(&deployment_root, bound_images)?; + + // The skopeo API expects absolute paths, so we make a temporary bind + let tmp_dest_var_abs = tempfile::tempdir()?; + let tmp_dest_var_abs: &Utf8Path = tmp_dest_var_abs.path().try_into()?; + let mut t = Task::new("Mounting deployment /var", "mount") + .args(["--bind", "/proc/self/fd/3"]) + .arg(tmp_dest_var_abs); + t.cmd.take_fd_n(Arc::new(OwnedFd::from(var)), 3); + t.run()?; + + // And an ephemeral place for the transient state + let tmp_runroot = tempfile::tempdir()?; + let tmp_runroot: &Utf8Path = tmp_runroot.path().try_into()?; + + // The destination (target stateroot) + container storage dest + let storage_dest = &format!( + "containers-storage:[overlay@{tmp_dest_var_abs}/lib/containers/storage+{tmp_runroot}]" + ); + + // Now copy each bound image from the host's container storage into the target. + for image in bound_images { + let image = image.image.as_str(); + Task::new(format!("Copying image to target: {}", image), "skopeo") + .arg("copy") + .arg(format!("{storage_src}{image}")) + .arg(format!("{storage_dest}{image}")) + .run()?; + } } Ok(()) @@ -1357,10 +1377,28 @@ async fn install_to_filesystem_impl(state: &State, rootfs: &mut RootSetup) -> Re .ok_or_else(|| anyhow!("No uuid for boot/root"))?; tracing::debug!("boot uuid={boot_uuid}"); + let bound_images = if state.config_opts.skip_bound_images { + Vec::new() + } else { + crate::boundimage::query_bound_images(&state.container_root)? + }; + tracing::debug!("bound images={bound_images:?}"); + + // Verify each bound image is present in the container storage + let bound_images = { + let mut r = Vec::with_capacity(bound_images.len()); + for image in bound_images { + let resolved = crate::boundimage::ResolvedBoundImage::from_image(&image).await?; + tracing::debug!("Resolved {}: {}", resolved.image, resolved.digest); + r.push(resolved) + } + r + }; + // Initialize the ostree sysroot (repo, stateroot, etc.) { let sysroot = initialize_ostree_root(state, rootfs).await?; - install_with_sysroot(state, rootfs, &sysroot, &boot_uuid).await?; + install_with_sysroot(state, rootfs, &sysroot, &boot_uuid, &bound_images).await?; // We must drop the sysroot here in order to close any open file // descriptors. }