diff --git a/lib/src/boundimage.rs b/lib/src/boundimage.rs index a5bf06d1..9a0d82e6 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 93403b75..66139ecd 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. }