From 1b2b5f7a6929d1379a8ea25bab15111d4a95d83c Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Sat, 2 Dec 2023 14:47:20 -0500 Subject: [PATCH] wip --- lib/src/podman_ostree.rs | 199 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 lib/src/podman_ostree.rs diff --git a/lib/src/podman_ostree.rs b/lib/src/podman_ostree.rs new file mode 100644 index 000000000..edddfc7ba --- /dev/null +++ b/lib/src/podman_ostree.rs @@ -0,0 +1,199 @@ +use std::cell::OnceCell; +use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use anyhow::{Context, Result}; + +use cap_std::fs::Dir; +use cap_std_ext::cap_std; +use cap_std_ext::cap_std::fs::DirBuilder; +use cap_std_ext::cap_std::io_lifetimes::AsFilelike; +use cap_std_ext::cap_tempfile::{TempDir, TempFile}; +use cap_std_ext::cmdext::CapStdExtCommandExt; +use cap_std_ext::dirext::CapStdExtDirExt; +use fn_error_context::context; +use ostree_ext::sysroot::SysrootLock; +use rustix::fd::AsFd; +use std::ffi::OsStr; +use std::os::unix::fs::MetadataExt; + +use crate::podman::PodmanInspectGraphDriver; +use crate::utils::sync_cmd_in_root; + +const OSTREE_CONTAINER_IMAGE_REF_PREFIX: &str = "ostree-container/image"; + +fn image_commit_ostree_ref(imageid: &str) -> String { + format!("{OSTREE_CONTAINER_IMAGE_REF_PREFIX}/{imageid}") +} + +struct MergeState<'a> { + trash: &'a Dir, + can_clone: bool, +} + +fn merge_layer( + layer: &Dir, + pathbuf: &mut std::path::PathBuf, + output: &Dir, + state: &MergeState, +) -> Result<()> { + let mut layer_trash = None; + let mut move_to_trash = |src: &Path, name: &OsStr| -> anyhow::Result<()> { + if layer_trash.is_none() { + layer_trash = Some(TempDir::new_in(state.trash)?); + } + let layer_trash = layer_trash.as_ref().unwrap(); + output + .rename(src, layer_trash, name) + .with_context(|| format!("Moving {name:?} to trash"))?; + Ok(()) + }; + for elt in layer.read_dir(&pathbuf)? { + let elt = elt?; + let name = elt.file_name(); + pathbuf.push(&name); + let src_meta = elt.metadata()?; + let src_ftype = src_meta.file_type(); + let target_meta = output + .symlink_metadata_optional(&pathbuf) + .context("Querying target")?; + if src_ftype.is_dir() { + let mut needs_create = true; + if let Some(target_meta) = target_meta { + if target_meta.is_dir() { + needs_create = false; + } else { + move_to_trash(&pathbuf, &name)?; + } + } + if needs_create { + let mut db = DirBuilder::new(); + db.mode(src_meta.mode()); + layer + .create_dir_with(&pathbuf, &db) + .with_context(|| format!("Creating {pathbuf:?}"))?; + } + // Now recurse + merge_layer(layer, pathbuf, output, state)?; + } else if (src_meta.mode() & libc::S_IFMT) == libc::S_IFCHR && src_meta.rdev() == 0 { + // It's a whiteout + if target_meta.is_some() { + move_to_trash(&pathbuf, &name)?; + } + } else { + if target_meta.is_some() { + move_to_trash(&pathbuf, &name)?; + } + if src_meta.is_symlink() { + let target = + cap_primitives::fs::read_link_contents(&layer.as_filelike_view(), &pathbuf) + .with_context(|| format!("Reading link {pathbuf:?}"))?; + cap_primitives::fs::symlink_contents(target, &output.as_filelike_view(), &pathbuf) + .with_context(|| format!("Writing symlink {pathbuf:?}"))?; + } else { + let mut openopts = cap_std::fs::OpenOptions::new(); + openopts.create_new(true); + openopts.mode(src_meta.mode()); + let src = layer + .open(&pathbuf) + .with_context(|| format!("Opening src {pathbuf:?}"))?; + if state.can_clone { + let dest = output + .open_with(&pathbuf, &openopts) + .with_context(|| format!("Opening dest {pathbuf:?}"))?; + rustix::fs::ioctl_ficlone(src.as_fd(), dest.as_fd()).context("Cloning")?; + } else { + layer + .hard_link(&pathbuf, output, &pathbuf) + .context("Hard linking")?; + } + } + } + assert!(pathbuf.pop()); + } + Ok(()) +} + +#[context("Squashing to tempdir")] +async fn generate_squashed_dir( + rootfs: &Dir, + graph: PodmanInspectGraphDriver, +) -> Result { + let ostree_tmp = &rootfs.open_dir("ostree/repo/tmp")?; + let td = TempDir::new_in(ostree_tmp)?; + // We put files/directories which should be deleted here; they're processed asynchronously + let trashdir = TempDir::new_in(ostree_tmp)?; + anyhow::ensure!(graph.name == "overlay"); + let rootfs = rootfs.try_clone()?; + let td = tokio::task::spawn_blocking(move || { + let can_clone = OnceCell::::new(); + for layer in graph.data.layers() { + // TODO: Does this actually work when operating on a non-default root? + let layer = layer.trim_start_matches('/'); + let layer = rootfs + .open_dir(layer) + .with_context(|| format!("Opening {layer}"))?; + if can_clone.get().is_none() { + let src = TempFile::new(&layer)?; + let dest = TempFile::new(&td)?; + let did_clone = + rustix::fs::ioctl_ficlone(src.as_file().as_fd(), dest.as_file().as_fd()) + .is_ok(); + can_clone.get_or_init(|| did_clone); + } + let mut pathbuf = PathBuf::from("."); + let mergestate = MergeState { + trash: &trashdir, + can_clone: *can_clone.get().unwrap(), + }; + merge_layer(&layer, &mut pathbuf, &td, &mergestate)?; + } + anyhow::Ok(td) + }) + .await??; + Ok(td) +} + +pub(crate) struct CommitResult { + pub(crate) manifest_digest: String, + pub(crate) version: Option, + pub(crate) ostree_ref: String, +} + +/// Given an image in containers-storage, generate an ostree commit from it +pub(crate) async fn commit_image_to_ostree( + sysroot: &SysrootLock, + imageid: &str, +) -> Result { + let rootfs = &Dir::reopen_dir(&crate::utils::sysroot_fd_borrowed(sysroot))?; + let cid = crate::podman::temporary_container_for_image(rootfs, imageid).await?; + let mount_path = &crate::podman::podman_mount(rootfs, &cid).await?; + let ostree_ref = image_commit_ostree_ref(imageid); + let mut inspect = crate::podman::podman_inspect(rootfs, imageid).await?; + let manifest_digest = inspect.digest; + let squashed = generate_squashed_dir(rootfs, inspect.graph_driver).await?; + let repo_fd = Arc::new(sysroot.repo().dfd_borrow().try_clone_to_owned()?); + let mut cmd = sync_cmd_in_root(&squashed, "ostree")?; + cmd.args([ + "--repo=/proc/self/fd/3", + "commit", + "--selinux-policy", + mount_path.as_str(), + "--branch", + ostree_ref.as_str(), + "--tree=dir=.", + ]); + cmd.take_fd_n(repo_fd, 3); + let mut cmd = tokio::process::Command::from(cmd); + cmd.kill_on_drop(true); + let st = cmd.status().await?; + if !st.success() { + anyhow::bail!("Failed to ostree commit: {st:?}") + } + Ok(CommitResult { + manifest_digest, + version: inspect.config.labels.remove("version"), + ostree_ref, + }) +}