forked from containers/bootc
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
199 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<cap_std_ext::cap_tempfile::TempDir> { | ||
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::<bool>::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<String>, | ||
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<CommitResult> { | ||
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, | ||
}) | ||
} |